-
-
Notifications
You must be signed in to change notification settings - Fork 457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add clean code example #2701
base: master
Are you sure you want to change the base?
Add clean code example #2701
Conversation
Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. |
WalkthroughThe pull request introduces several significant changes to the Changes
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 26
🧹 Outside diff range and nitpick comments (39)
clean-code/app/main.go (1)
3-11
: Consider enhancing application lifecycle managementWhile the imports are well-organized, consider adding support for graceful shutdown and more robust error handling:
- Add
os/signal
andsyscall
for handling shutdown signals- Consider using a more robust logging package like
zerolog
orzap
Example implementation:
import ( "context" "log" "os" "os/signal" "syscall" "app/datasources" "app/datasources/database" "app/server" )clean-code/app/config.go (3)
1-3
: Consider moving configuration to a dedicated packageFor better separation of concerns and adherence to clean architecture principles, consider moving this configuration code to a dedicated package (e.g.,
config
orinternal/config
). This would make it more reusable and maintainable, especially in larger applications.-package main +package config import "os"
5-8
: Add field documentation for better clarityConsider adding documentation comments for the struct and its fields to improve code clarity and maintainability.
+// Configuration holds the application configuration settings type Configuration struct { + // Port specifies the server port to listen on Port string + // DatabaseURL specifies the connection string for the database DatabaseURL string }
17-22
: Consider adding debug loggingThe environment variable lookup implementation is correct, but adding debug logging could help with troubleshooting configuration issues in production.
func getEnvOrDefault(key, defaultValue string) string { if value, exists := os.LookupEnv(key); exists { + log.Printf("Config: %s=%s\n", key, value) return value } + log.Printf("Config: %s not set, using default: %s\n", key, defaultValue) return defaultValue }clean-code/app/server/server_test.go (1)
14-16
: Consider improving context and resource management.The test initializes the server with an empty context and DataSources, which might not represent real-world scenarios accurately.
Consider these improvements:
func TestGetStatus(t *testing.T) { - app := NewServer(context.Background(), &datasources.DataSources{}) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ds := &datasources.DataSources{} + defer ds.Close() // if applicable + + app := NewServer(ctx, ds)clean-code/app/server/services/books_mock.go (3)
11-13
: Add documentation for the mock struct.Consider adding a doc comment explaining the purpose of this mock and how it should be used in tests.
+// BooksServiceMock provides a mock implementation of the BooksService interface +// for testing purposes. It uses testify/mock for implementing mock behavior. type BooksServiceMock struct { mock.Mock }
15-21
: Add safe type assertion to prevent potential panics.The current type assertion could panic if the mock is set up incorrectly. While this is a test mock, safer type assertions would make tests more maintainable.
func (m *BooksServiceMock) GetBooks(ctx context.Context) ([]domain.Book, error) { args := m.Called(ctx) if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).([]domain.Book), args.Error(1) + books, ok := args.Get(0).([]domain.Book) + if !ok { + return nil, fmt.Errorf("invalid mock setup: expected []domain.Book, got %T", args.Get(0)) + } + return books, args.Error(1) }
23-26
: Consider adding validation for the newBook parameter.While the implementation is correct, consider adding validation to ensure the mock behaves similarly to the real service when receiving invalid input.
func (m *BooksServiceMock) SaveBook(ctx context.Context, newBook domain.Book) error { + if newBook.ID == "" { + return fmt.Errorf("mock validation: book ID cannot be empty") + } args := m.Called(ctx, newBook) return args.Error(0) }clean-code/app/datasources/database/db_mock.go (1)
26-27
: Enhance mock cleanup methodThe empty implementation misses opportunities for better testing support.
Consider this enhanced implementation:
+// CloseConnections is a mock implementation that tracks connection closure. +// In tests, you can verify if cleanup was called: +// mock.AssertCalled(t, "CloseConnections") func (m *DatabaseMock) CloseConnections() { + m.Called() }clean-code/app/server/server.go (2)
3-11
: Consider adding error handling utilitiesFor a production-ready clean code example, consider importing error handling utilities (like
errors
package or a custom error package) to properly handle and wrap errors that might occur during database operations or request processing.
15-16
: Consider adding essential middleware and documenting versioning strategyFor a production-ready example, consider adding essential middleware for:
- Request logging
- Panic recovery
- CORS handling
- Request timeout
- Authentication/Authorization
Also, document the API versioning strategy (currently using path-based v1).
Example middleware setup:
app := fiber.New() + app.Use(recover.New()) + app.Use(logger.New()) + app.Use(cors.New()) + app.Use(timeout.New(timeout.Config{ + TimeoutHandler: func(c *fiber.Ctx) error { + return c.Status(fiber.StatusRequestTimeout).JSON(fiber.Map{ + "error": "Request Timeout", + }) + }, + Timeout: 5 * time.Second, + })) apiRoutes := app.Group("/api")clean-code/app/datasources/database/memory_db.go (2)
5-11
: Consider improving documentation and visibility.While the implementation is good, consider these improvements:
- Add godoc-style comments explaining the purpose and usage of this constructor
- Consider exporting the constructor if it's meant to be used by other packages for testing purposes
-// This is just an example and not for production use +// NewMemoryDB returns an in-memory implementation of the Database interface. +// It is intended for testing and example purposes only, not for production use. +// The implementation stores books in memory and provides basic CRUD operations. -func newMemoryDB() Database { +func NewMemoryDB() Database {
31-32
: Document the empty CloseConnections method.Add documentation to explain why this method is empty for the in-memory implementation.
+// CloseConnections is a no-op for memory database as there are no connections to close. +// It exists to satisfy the Database interface. func (db *memoryDB) CloseConnections() { }clean-code/app/datasources/database/db.go (3)
9-12
: Consider enhancing the Book struct with tags and documentation.The struct could benefit from:
- Field tags for JSON/DB mapping
- GoDoc comments
- Title field validation
Consider applying these improvements:
+// Book represents a book entity in the system type Book struct { - ID int - Title string + ID int `json:"id" db:"id"` + Title string `json:"title" db:"title"` } +// ValidateBook ensures the book data is valid +func (b Book) ValidateBook() error { + if strings.TrimSpace(b.Title) == "" { + return errors.New("title cannot be empty") + } + return nil +}
14-18
: Consider enhancing the Database interface with documentation and pagination.The interface follows good practices with context usage and error handling. However, consider these improvements:
- Add GoDoc comments
- Implement pagination for LoadAllBooks to handle large datasets
- Return the created book from CreateBook for immediate use
Consider applying these changes:
+// Database defines the interface for database operations type Database interface { - LoadAllBooks(ctx context.Context) ([]Book, error) + // LoadAllBooks retrieves a paginated list of books + LoadAllBooks(ctx context.Context, offset, limit int) ([]Book, error) - CreateBook(ctx context.Context, newBook Book) error + // CreateBook adds a new book and returns the created entity + CreateBook(ctx context.Context, newBook Book) (*Book, error) + // CloseConnections closes all active database connections CloseConnections() }
20-34
: Consider implementing a database factory pattern with dependency injection.The current implementation could be improved by:
- Using a factory pattern with registered database drivers
- Implementing dependency injection for better testability
- Adding support for database configuration beyond just the URL
Consider this alternative approach:
type DBDriver interface { Connect(ctx context.Context, config DBConfig) (Database, error) } type DBConfig struct { URL string MaxConns int Timeout time.Duration // Add other configuration options } type DatabaseFactory struct { drivers map[string]DBDriver } func (f *DatabaseFactory) Register(name string, driver DBDriver) { f.drivers[name] = driver } func (f *DatabaseFactory) Create(ctx context.Context, driverName string, config DBConfig) (Database, error) { driver, exists := f.drivers[driverName] if !exists { return nil, fmt.Errorf("unknown database driver: %s", driverName) } return driver.Connect(ctx, config) }clean-code/app/datasources/database/db_test.go (2)
11-15
: Consider enhancing the memory DB test coverage.While the type check is useful, consider adding:
- Verification that the database is properly initialized and operational.
- Interface compliance check instead of concrete type check.
func TestNewDatabase_MemoryDB(t *testing.T) { ctx := context.Background() db := NewDatabase(ctx, "") - assert.Equal(t, "*database.memoryDB", reflect.TypeOf(db).String()) + // Verify interface compliance + _, ok := db.(Database) + assert.True(t, ok) + + // Verify database is operational + book := Book{Title: "Test Book"} + _, err := db.AddBook(ctx, book) + assert.NoError(t, err) }
31-34
: Improve helper function documentation and completeness.The helper function could be enhanced:
- Add documentation explaining its purpose and usage.
- Consider validating all Book fields.
- Use a more specific name indicating what's being asserted.
-func assertBook(t *testing.T, book Book, expectedID int, expected Book) { +// assertBookEquals is a test helper that verifies if a book matches the expected values +// by comparing all relevant fields +func assertBookEquals(t *testing.T, actual Book, expectedID int, expected Book) { + t.Helper() // Mark as test helper for better error reporting assert.Equal(t, expectedID, book.ID) assert.Equal(t, expected.Title, book.Title) + // Add other relevant field comparisons + assert.Equal(t, expected.Author, book.Author) + assert.Equal(t, expected.ISBN, book.ISBN) }clean-code/app/config_test.go (2)
22-30
: Consider enhancing test efficiency and coverage.Suggestions for improvement:
- Enable parallel test execution using
t.Parallel()
- Convert to table-driven tests to cover multiple scenarios
- Add assertions for other potential default values
Example refactor:
func TestNewConfiguration_Defaults(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + unsetEnv []string + want Configuration + }{ + { + name: "all defaults", + unsetEnv: []string{"PORT", "DATABASE_URL"}, + want: Configuration{Port: "3000", DatabaseURL: ""}, + }, + // Add more test cases here + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, env := range tt.unsetEnv { + os.Unsetenv(env) + } + got := NewConfiguration() + assert.Equal(t, tt.want, got) + }) + } - os.Unsetenv("PORT") - os.Unsetenv("DATABASE_URL") - - conf := NewConfiguration() - - assert.Equal(t, "3000", conf.Port) - assert.Equal(t, "", conf.DatabaseURL) }
32-41
: Add test cases for edge cases.The current test covers basic scenarios, but consider adding test cases for:
- Empty string values
- Whitespace-only values
- Special characters in environment variables
Example additional test cases:
func TestGetEnvOrDefault(t *testing.T) { t.Parallel() tests := []struct { name string envKey string envValue string defaultValue string want string }{ { name: "existing env", envKey: "TEST_ENV", envValue: "value", defaultValue: "default", want: "value", }, { name: "empty string", envKey: "TEST_ENV_EMPTY", envValue: "", defaultValue: "default", want: "", }, { name: "whitespace only", envKey: "TEST_ENV_SPACE", envValue: " ", defaultValue: "default", want: " ", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.envValue != "" { os.Setenv(tt.envKey, tt.envValue) defer os.Unsetenv(tt.envKey) } got := getEnvOrDefault(tt.envKey, tt.defaultValue) assert.Equal(t, tt.want, got) }) } }clean-code/app/server/services/books.go (2)
11-14
: Add interface documentation.Consider adding GoDoc comments to document the interface and its methods. This will improve code maintainability and help other developers understand the service's responsibilities.
+// BooksService defines the interface for book management operations type BooksService interface { + // GetBooks retrieves all books from the storage GetBooks(ctx context.Context) ([]domain.Book, error) + // SaveBook persists a new book to the storage SaveBook(ctx context.Context, newBook domain.Book) error }
16-22
: Add nil check in constructor.Consider adding validation to prevent nil database injection, which could cause runtime panics.
func NewBooksService(db database.Database) BooksService { + if db == nil { + panic("database is required") + } return &booksService{db: db} }clean-code/app/datasources/database/memory_db_test.go (2)
10-15
: Enhance test coverage and naming.While the basic happy path is covered, consider these improvements:
- Rename test to describe the scenario (e.g.,
TestMemoryDB_LoadBooks_EmptyDatabase
)- Add test cases for error scenarios
- Test context cancellation handling
Example improvement:
-func TestMemoryDB_LoadBooks(t *testing.T) { +func TestMemoryDB_LoadBooks_EmptyDatabase(t *testing.T) { db := newMemoryDB() books, err := db.LoadAllBooks(context.Background()) assert.Nil(t, err) assert.Equal(t, 0, len(books)) } + +func TestMemoryDB_LoadBooks_CancelledContext(t *testing.T) { + db := newMemoryDB() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + books, err := db.LoadAllBooks(ctx) + assert.Error(t, err) + assert.Nil(t, books) +}
1-44
: Consider enhancing test organization for clean architecture.For a clean code example, consider these architectural improvements:
- Add a test suite using
suite.Suite
from testify for better organization- Include test fixtures or factories for book creation
- Add integration tests with the actual database interface
- Document test scenarios in BDD style (Given/When/Then)
clean-code/app/server/handlers/books.go (2)
12-24
: Consider enhancing error handling and request validation.The handler implementation is solid with good separation of concerns and proper context propagation. However, there are a few potential improvements:
- The error message "internal error" could be more specific while still not exposing internal details
- Consider adding request validation for query parameters (if any)
- Consider adding context timeout handling
Here's a suggested improvement:
func GetBooks(service services.BooksService) fiber.Handler { return func(c *fiber.Ctx) error { + // Add timeout to prevent long-running queries + ctx, cancel := context.WithTimeout(c.UserContext(), 5*time.Second) + defer cancel() + - books, err := service.GetBooks(c.UserContext()) + books, err := service.GetBooks(ctx) if err != nil { - log.Printf("GetBooks failed: %v", err) + log.Printf("Failed to retrieve books: %v", err) return sendError(c, fiber.StatusInternalServerError, "internal error") } return c.JSON(domain.BooksResponse{ Books: books, }) } }
43-47
: Consider enhancing error response structure.The error helper is clean but could be more informative for API consumers.
Consider this enhancement:
-func sendError(c *fiber.Ctx, code int, message string) error { +func sendError(c *fiber.Ctx, code int, message string) error { + c.Set("Content-Type", "application/json") return c.Status(code).JSON(domain.ErrorResponse{ - Error: message, + Error: message, + Code: code, + RequestID: c.GetRespHeader("X-Request-ID"), }) }clean-code/app/server/services/books_test.go (5)
14-22
: Enhance test coverage and documentation.Consider the following improvements:
- Add a test description using
t.Run()
for better test organization- Verify the actual content of the returned book, not just the length
- Validate that the context is properly passed to the database layer
Here's a suggested improvement:
func TestGetBooks(t *testing.T) { - mockDB := new(database.DatabaseMock) - mockDB.On("LoadAllBooks", mock.Anything).Return([]database.Book{{Title: "Title"}}, nil) - - service := NewBooksService(mockDB) - books, err := service.GetBooks(context.Background()) - assert.Nil(t, err) - assert.Len(t, books, 1) + t.Run("successfully retrieves books from database", func(t *testing.T) { + expectedBook := database.Book{Title: "Title"} + mockDB := new(database.DatabaseMock) + mockDB.On("LoadAllBooks", mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + })).Return([]database.Book{expectedBook}, nil) + + service := NewBooksService(mockDB) + books, err := service.GetBooks(context.Background()) + + assert.NoError(t, err) + assert.Len(t, books, 1) + assert.Equal(t, expectedBook.Title, books[0].Title) + mockDB.AssertExpectations(t) + })
24-31
: Improve error testing specificity.The test could be more robust by:
- Using a specific error instead of generic
assert.AnError
- Adding a test description
- Verifying the error type/message returned to ensure proper error handling
Here's a suggested improvement:
func TestGetBooks_Fails(t *testing.T) { - mockDB := new(database.DatabaseMock) - mockDB.On("LoadAllBooks", mock.Anything).Return(nil, assert.AnError) - - service := NewBooksService(mockDB) - _, err := service.GetBooks(context.Background()) - assert.NotNil(t, err) + t.Run("returns error when database fails", func(t *testing.T) { + expectedErr := errors.New("database connection failed") + mockDB := new(database.DatabaseMock) + mockDB.On("LoadAllBooks", mock.Anything).Return(nil, expectedErr) + + service := NewBooksService(mockDB) + _, err := service.GetBooks(context.Background()) + + assert.Error(t, err) + assert.Equal(t, expectedErr.Error(), err.Error()) + mockDB.AssertExpectations(t) + })
33-40
: Enhance save book test coverage.Consider improving the test by:
- Adding test cases for different book data
- Verifying all book fields being passed to the database
- Adding proper test descriptions
Here's a suggested improvement:
func TestSaveBook(t *testing.T) { - mockDB := new(database.DatabaseMock) - mockDB.On("CreateBook", mock.Anything, database.Book{Title: "Title"}).Return(nil) - - service := NewBooksService(mockDB) - err := service.SaveBook(context.Background(), domain.Book{Title: "Title"}) - assert.Nil(t, err) + testCases := []struct { + name string + book domain.Book + }{ + { + name: "successfully saves book with title only", + book: domain.Book{Title: "Title"}, + }, + { + name: "successfully saves book with all fields", + book: domain.Book{Title: "Title", Author: "Author", ISBN: "1234567890"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB := new(database.DatabaseMock) + mockDB.On("CreateBook", mock.Anything, mock.MatchedBy(func(b database.Book) bool { + return b.Title == tc.book.Title && + b.Author == tc.book.Author && + b.ISBN == tc.book.ISBN + })).Return(nil) + + service := NewBooksService(mockDB) + err := service.SaveBook(context.Background(), tc.book) + + assert.NoError(t, err) + mockDB.AssertExpectations(t) + }) + }
42-49
: Improve error handling test coverage.The test could be enhanced by:
- Testing different error scenarios (e.g., validation errors, database constraints)
- Using specific errors instead of generic
assert.AnError
- Adding proper test descriptions
Here's a suggested improvement:
func TestSaveBook_Fails(t *testing.T) { - mockDB := new(database.DatabaseMock) - mockDB.On("CreateBook", mock.Anything, database.Book{Title: "Title"}).Return(assert.AnError) - - service := NewBooksService(mockDB) - err := service.SaveBook(context.Background(), domain.Book{Title: "Title"}) - assert.NotNil(t, err) + testCases := []struct { + name string + book domain.Book + expectedError error + }{ + { + name: "fails when database error occurs", + book: domain.Book{Title: "Title"}, + expectedError: errors.New("database error"), + }, + { + name: "fails when duplicate book exists", + book: domain.Book{Title: "Title"}, + expectedError: errors.New("duplicate book"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB := new(database.DatabaseMock) + mockDB.On("CreateBook", mock.Anything, mock.MatchedBy(func(b database.Book) bool { + return b.Title == tc.book.Title + })).Return(tc.expectedError) + + service := NewBooksService(mockDB) + err := service.SaveBook(context.Background(), tc.book) + + assert.Error(t, err) + assert.Equal(t, tc.expectedError.Error(), err.Error()) + mockDB.AssertExpectations(t) + }) + }
1-49
: Consider architectural improvements for better test coverage.While the test structure is good, consider these architectural improvements:
- Add separate test cases for domain.Book to database.Book mapping
- Implement more thorough context handling (timeouts, cancellation)
- Create custom error types for better error handling and verification
- Consider adding integration tests alongside these unit tests
Would you like assistance in implementing any of these improvements?
clean-code/app/datasources/database/postgres_db.go (3)
12-16
: Consider enhancing the interface with transaction supportWhile the current interface covers basic operations, consider adding transaction support for maintaining data consistency in complex operations. Also, the
Close
method should return an error to handle cleanup failures.type PostgresPool interface { Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) - Close() + Close() error + Begin(ctx context.Context) (pgx.Tx, error) }
52-58
: Add support for batch operations in CreateBookThe current implementation doesn't support batch inserts, which could be inefficient for multiple book creations.
+const queryCreateBook = "INSERT INTO books (title) VALUES ($1)" + func (db *postgresDB) CreateBook(ctx context.Context, newBook Book) error { - _, err := db.pool.Exec(ctx, "INSERT INTO books (title) VALUES ($1)", newBook.Title) + _, err := db.pool.Exec(ctx, queryCreateBook, newBook.Title) if err != nil { return fmt.Errorf("failed to insert book: %w", err) } return nil } + +func (db *postgresDB) CreateBooks(ctx context.Context, books []Book) error { + batch := &pgx.Batch{} + for _, book := range books { + batch.Queue(queryCreateBook, book.Title) + } + + results := db.pool.SendBatch(ctx, batch) + defer results.Close() + + return results.Close() +}
1-62
: Consider implementing repository pattern more strictlyWhile the current implementation is clean, consider these architectural improvements:
- Move SQL queries to a separate constants file for better maintenance
- Add a proper domain model layer to separate database entities from domain entities
- Consider adding a transaction middleware for handling complex operations
Would you like me to provide an example of how to implement these architectural improvements?
clean-code/app/datasources/database/postgres_db_test.go (4)
15-17
: Consider using parameterized queries in test expectationsWhile the current query works, consider using parameterized queries in the test expectations to better reflect secure coding practices that should be used in the actual implementation.
-mockPool.ExpectQuery("SELECT id, title FROM books"). +mockPool.ExpectQuery("SELECT id, title FROM books WHERE 1=$1"). + WithArgs(1).
11-28
: Consider using table-driven testsThe test could be more comprehensive by using table-driven tests to cover multiple scenarios (empty result, multiple books, etc.).
Here's a suggested refactor:
func TestPostgresDB_GetBooks(t *testing.T) { tests := []struct { name string mockRows func() *pgxmock.Rows want []Book wantErr bool }{ { name: "single book", mockRows: func() *pgxmock.Rows { return pgxmock.NewRows([]string{"id", "title"}).AddRow(1, "book1") }, want: []Book{{ID: 1, Title: "book1"}}, }, { name: "multiple books", mockRows: func() *pgxmock.Rows { return pgxmock.NewRows([]string{"id", "title"}). AddRow(1, "book1"). AddRow(2, "book2") }, want: []Book{{ID: 1, Title: "book1"}, {ID: 2, Title: "book2"}}, }, { name: "no books", mockRows: func() *pgxmock.Rows { return pgxmock.NewRows([]string{"id", "title"}) }, want: []Book{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPool, err := pgxmock.NewPool() assert.Nil(t, err) mockPool.ExpectQuery("SELECT id, title FROM books"). WillReturnRows(tt.mockRows()) db := postgresDB{pool: mockPool} result, err := db.LoadAllBooks(context.Background()) if tt.wantErr { assert.Error(t, err) return } assert.Nil(t, err) assert.Equal(t, tt.want, result) assert.Nil(t, mockPool.ExpectationsWereMet()) }) } }
34-35
: Consider using specific database errorsInstead of using
assert.AnError
, consider using specific database errors (likepgx.ErrNoRows
or custom errors) to make the test more realistic and meaningful. Also, the error message assertion could be more specific.- WillReturnError(assert.AnError) + WillReturnError(&pgconn.PgError{ + Code: "42P01", + Message: "relation \"books\" does not exist", + }) - assert.ErrorContains(t, err, "failed to query books table") + assert.ErrorContains(t, err, "failed to query books table: relation \"books\" does not exist")Also applies to: 41-42
51-53
: Make SQL expectations more explicitThe current SQL expectation is too permissive. Consider making it more explicit to catch potential query changes.
- mockPool.ExpectExec("INSERT INTO books"). + mockPool.ExpectExec("INSERT INTO books (title) VALUES ($1)"). WithArgs("book1"). WillReturnResult(pgxmock.NewResult("INSERT", 1))clean-code/app/server/handlers/books_test.go (1)
20-33
: Consider adding more test cases for GetBooks.While the happy path is tested, consider adding test cases for:
- Multiple books in response
- Empty books list
func TestGetBooks(t *testing.T) { testCases := []struct { name string books []domain.Book expected int }{ { name: "single book", books: []domain.Book{{Title: "Title"}}, expected: 1, }, { name: "multiple books", books: []domain.Book{{Title: "Title1"}, {Title: "Title2"}}, expected: 2, }, { name: "empty list", books: []domain.Book{}, expected: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockService := new(services.BooksServiceMock) mockService.On("GetBooks", mock.Anything).Return(tc.books, nil) app := fiber.New() app.Get(booksRoute, GetBooks(mockService)) resp, err := app.Test(httptest.NewRequest("GET", booksRoute, nil)) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) body := bodyFromResponse[domain.BooksResponse](t, resp) assert.Len(t, body.Books, tc.expected) }) } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
⛔ Files ignored due to path filters (3)
clean-code/app/go.mod
is excluded by!**/*.mod
clean-code/app/go.sum
is excluded by!**/*.sum
,!**/*.sum
clean-code/docker-compose.yml
is excluded by!**/*.yml
📒 Files selected for processing (24)
clean-code/.gitignore
(1 hunks)clean-code/Dockerfile-local
(1 hunks)clean-code/README.md
(1 hunks)clean-code/app/config.go
(1 hunks)clean-code/app/config_test.go
(1 hunks)clean-code/app/datasources/data_sources.go
(1 hunks)clean-code/app/datasources/database/db.go
(1 hunks)clean-code/app/datasources/database/db_mock.go
(1 hunks)clean-code/app/datasources/database/db_test.go
(1 hunks)clean-code/app/datasources/database/memory_db.go
(1 hunks)clean-code/app/datasources/database/memory_db_test.go
(1 hunks)clean-code/app/datasources/database/postgres_db.go
(1 hunks)clean-code/app/datasources/database/postgres_db_test.go
(1 hunks)clean-code/app/main.go
(1 hunks)clean-code/app/server/domain/books.go
(1 hunks)clean-code/app/server/domain/errors.go
(1 hunks)clean-code/app/server/handlers/books.go
(1 hunks)clean-code/app/server/handlers/books_test.go
(1 hunks)clean-code/app/server/server.go
(1 hunks)clean-code/app/server/server_test.go
(1 hunks)clean-code/app/server/services/books.go
(1 hunks)clean-code/app/server/services/books_mock.go
(1 hunks)clean-code/app/server/services/books_test.go
(1 hunks)clean-code/db/init_db.sql
(1 hunks)
✅ Files skipped from review due to trivial changes (6)
- clean-code/.gitignore
- clean-code/Dockerfile-local
- clean-code/app/datasources/data_sources.go
- clean-code/app/server/domain/books.go
- clean-code/app/server/domain/errors.go
- clean-code/db/init_db.sql
🧰 Additional context used
🪛 LanguageTool
clean-code/README.md
[uncategorized] ~16-~16: Loose punctuation mark.
Context: ... ### Endpoints - GET /api/v1/books
: Retrieves a list of all books. ```sh ...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~21-~21: Loose punctuation mark.
Context: ...i/v1/books ``` - POST /api/v1/books
: Adds a new book to the collection. ``...
(UNLIKELY_OPENING_PUNCTUATION)
🪛 Markdownlint (0.35.0)
clean-code/README.md
3-3: null
Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (26)
clean-code/app/main.go (1)
14-14
: Verify configuration implementation details
Let's verify the implementation of NewConfiguration()
and its error handling capabilities.
✅ Verification successful
Configuration implementation is properly structured and safe
The NewConfiguration()
function is well-implemented with:
- A clear struct definition with Port and DatabaseURL fields
- Safe environment variable handling using
getEnvOrDefault
- Default values for required configurations
- Proper test coverage in
config_test.go
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for NewConfiguration implementation
ast-grep --pattern 'func NewConfiguration() $$$'
# Check if configuration uses environment variables
rg -l "os.Getenv" clean-code/
Length of output: 418
Script:
#!/bin/bash
# Let's check the Configuration struct definition and getEnvOrDefault implementation
rg -A 5 "type Configuration struct" clean-code/app/
# Search for getEnvOrDefault implementation
ast-grep --pattern 'func getEnvOrDefault($$$) $$$'
# Check if there are any direct usages of Configuration
rg "Configuration" clean-code/app/
Length of output: 1318
clean-code/app/config.go (1)
1-22
: Verify configuration usage across the application
Let's verify how this configuration is used across the application to ensure proper error handling and consistent usage.
✅ Verification successful
Let me gather more information about the database connection handling to ensure proper error handling is in place.
Configuration implementation is well-structured with proper error handling
The verification reveals a well-implemented configuration system with appropriate error handling:
- Configuration is properly centralized with no direct environment variable access in the clean-code app
- Database connection has proper error handling with fallback to in-memory database when URL is empty
- Implementation includes comprehensive test coverage for both valid and invalid database configurations
- The system gracefully handles missing environment variables with sensible defaults
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for Configuration struct usage
echo "Checking Configuration struct usage:"
rg -l "Configuration" --type go
# Search for environment variable usage to ensure centralization
echo -e "\nChecking for direct environment variable access that should use Configuration:"
rg -l "os.Getenv|os.LookupEnv" --type go | grep -v "config.go"
# Check for proper error handling around database connection
echo -e "\nChecking database connection handling:"
rg -B 2 -A 2 "DatabaseURL" --type go
Length of output: 2661
Script:
#!/bin/bash
# Check database connection implementation
echo "Checking database connection implementation:"
ast-grep --pattern 'func NewDatabase($_, $url string) $_'
# Check for error handling in database connection
echo -e "\nChecking error handling in database connection:"
rg -B 3 -A 3 "NewDatabase" --type go
# Check if there's any validation for DatabaseURL
echo -e "\nChecking for DatabaseURL validation:"
rg -B 2 -A 2 "DATABASE_URL" --type go
Length of output: 4054
clean-code/app/server/server_test.go (2)
1-12
: LGTM! Package and imports are well-organized.
The package declaration and import organization follow Go best practices, with standard library imports, internal packages, and external dependencies properly grouped.
17-24
: 🛠️ Refactor suggestion
Enhance test coverage with table-driven tests.
The current test only covers the happy path. Consider adding test cases for different scenarios and improving the response structure.
Here's a suggested improvement:
-func TestGetStatus(t *testing.T) {
- app := NewServer(context.Background(), &datasources.DataSources{})
-
- resp, err := app.Test(httptest.NewRequest("GET", "/api/status", nil))
- assert.Nil(t, err)
- assert.Equal(t, 200, resp.StatusCode)
-
- body, err := io.ReadAll(resp.Body)
- assert.Nil(t, err)
- assert.Equal(t, "ok", string(body))
+func TestGetStatus(t *testing.T) {
+ tests := []struct {
+ name string
+ method string
+ expectedStatus int
+ expectedBody string
+ }{
+ {
+ name: "success case",
+ method: "GET",
+ expectedStatus: http.StatusOK,
+ expectedBody: "ok",
+ },
+ {
+ name: "wrong method",
+ method: "POST",
+ expectedStatus: http.StatusMethodNotAllowed,
+ expectedBody: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ app := NewServer(ctx, &datasources.DataSources{})
+ req := httptest.NewRequest(tt.method, "/api/status", nil)
+ resp, err := app.Test(req)
+
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedStatus, resp.StatusCode)
+
+ if tt.expectedBody != "" {
+ body, err := io.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedBody, string(body))
+ }
+ })
+ }
}
Additionally, consider:
- Using structured JSON responses instead of plain text
- Adding test cases for server shutdown scenarios
- Testing response headers
Let's verify if the status endpoint is defined in the server implementation:
clean-code/app/server/services/books_mock.go (2)
1-9
: LGTM!
The package declaration and imports are well-organized and follow Go best practices.
11-26
: Verify interface compliance.
Let's ensure this mock correctly implements all methods of the BooksService interface.
✅ Verification successful
Mock implementation matches the interface perfectly
The BooksService interface defines exactly two methods:
GetBooks(ctx context.Context) ([]domain.Book, error)
SaveBook(ctx context.Context, newBook domain.Book) error
The BooksServiceMock implements both methods with matching signatures, making it a complete and correct implementation of the interface.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify that BooksServiceMock implements all methods of BooksService interface
# Search for BooksService interface definition
echo "Searching for BooksService interface definition..."
ast-grep --pattern 'type BooksService interface {
$$$
}'
# Search for any additional methods that might need to be mocked
echo "Searching for method calls on BooksService..."
rg -t go "BooksService\." -A 2
Length of output: 621
clean-code/app/datasources/database/db_mock.go (3)
1-8
: LGTM!
The package name and imports are appropriate for a database mock implementation.
9-11
: LGTM!
The mock struct is correctly defined with embedded mock.Mock.
1-27
: Verify interface implementation completeness
Let's ensure this mock fully implements the Database interface.
✅ Verification successful
Based on the shell script results, I can see the Database interface definition in clean-code/app/datasources/database/db.go
which shows the following methods:
LoadAllBooks(ctx context.Context) ([]Book, error)
CreateBook(ctx context.Context, newBook Book) error
CloseConnections()
Comparing this with the mock implementation in db_mock.go
, we can see that all methods are properly implemented:
LoadAllBooks
- Implemented with correct signature and mock behaviorCreateBook
- Implemented with correct signature and mock behaviorCloseConnections
- Implemented as an empty method (which is valid for a mock)
Mock implementation is complete and correct
The DatabaseMock struct fully implements all methods of the Database interface with proper signatures and appropriate mock behaviors.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify Database interface implementation
# Expected: Find Database interface definition and compare methods
# Find the interface definition
echo "Looking for Database interface definition..."
rg -p "type\s+Database\s+interface\s*{" -A 10
# Find any other methods that might need mocking
echo "Looking for interface method usages..."
ast-grep --pattern 'interface { $$$ }'
Length of output: 17310
clean-code/app/server/server.go (1)
1-24
: Verify test coverage for server setup
Let's verify the existence of tests for the server setup and endpoints.
✅ Verification successful
Test coverage is comprehensive and well-structured
The codebase has thorough test coverage across all layers:
- Server setup and health check endpoint (/api/status) are tested in
server_test.go
- Book handlers have extensive tests in
handlers/books_test.go
covering success and error cases - Book service layer is well tested in
services/books_test.go
with proper mocking
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for test files related to server setup
echo "Searching for server tests..."
fd -e go -g "*server*test.go" clean-code/app/server
# Search for specific test cases
echo "Searching for specific test cases..."
rg -l "TestNewServer|TestHealthCheck|TestGetBooks|TestAddBook" clean-code/app/server
Length of output: 419
Script:
#!/bin/bash
# Let's check the content of server_test.go to verify test coverage
echo "Content of server_test.go:"
cat clean-code/app/server/server_test.go
# Check test coverage for handlers and services
echo -e "\nContent of handlers/books_test.go:"
cat clean-code/app/server/handlers/books_test.go
echo -e "\nContent of services/books_test.go:"
cat clean-code/app/server/services/books_test.go
Length of output: 5010
clean-code/app/datasources/database/memory_db.go (1)
1-32
: Verify Database interface implementation.
Let's ensure that memoryDB correctly implements all methods of the Database interface.
✅ Verification successful
The output shows the Database interface but seems incomplete in showing memoryDB methods. Let me try a different pattern to ensure we capture all the methods.
Database interface implementation is complete and correct
The memoryDB type correctly implements all methods required by the Database interface:
- LoadAllBooks(ctx context.Context) ([]Book, error)
- CreateBook(ctx context.Context, newBook Book) error
- CloseConnections()
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify Database interface implementation
# Expected: Find the Database interface definition and compare with memoryDB methods
# Find the Database interface definition
echo "Database interface definition:"
ast-grep --pattern 'type Database interface {
$$$
}'
# Find all methods of memoryDB
echo -e "\nmemoryDB method implementations:"
ast-grep --pattern 'func (db *memoryDB) $$$'
Length of output: 632
Script:
#!/bin/bash
# Search for memoryDB methods with a more specific pattern
echo "memoryDB methods:"
rg "func \(db \*memoryDB\)" -A 1
# Also verify the struct definition
echo -e "\nmemoryDB struct definition:"
ast-grep --pattern 'type memoryDB struct {
$$$
}'
Length of output: 1018
clean-code/app/datasources/database/db.go (1)
1-8
: LGTM! Clean package structure and imports.
The package name matches the directory name and imports are minimal.
clean-code/app/datasources/database/db_test.go (1)
1-9
: LGTM!
The package declaration and imports are appropriate for the test file.
clean-code/app/server/services/books.go (2)
1-9
: LGTM! Clean package structure and imports.
The package organization follows Go best practices with clear separation between standard library and project imports.
24-38
: Verify complete domain model mapping.
The current implementation only maps the Title
field. Ensure this is intentional and that no other important book fields (e.g., ID, Author, ISBN) are missing from the domain model.
✅ Verification successful
The domain model is intentionally simplified to only include the Title field
Looking at the relevant models in the codebase:
- Domain model (
clean-code/app/server/domain/books.go
) only definesTitle
field - Database model (
clean-code/app/datasources/database/db.go
) hasID
andTitle
fields
The current implementation correctly maps all fields from the domain model, and the simplification appears to be intentional for this clean code example. The other Book structs found in the codebase are from different example projects and are not related to this implementation.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check for other potential book fields in the domain and database models
rg -A 5 "type Book struct"
Length of output: 3738
clean-code/app/datasources/database/memory_db_test.go (2)
1-8
: LGTM! Clean imports and proper package naming.
The imports are minimal and appropriate for the testing requirements.
17-27
: Verify the assertBook helper function implementation.
The test uses an assertBook
helper function that isn't visible in the current context. Let's verify its implementation to ensure proper assertions.
Additionally, consider enhancing the test coverage:
- Validate book ID assignment logic
- Test error scenarios (e.g., duplicate books if that's a constraint)
✅ Verification successful
assertBook
helper function implementation is verified and adequate
The helper function correctly validates both the ID assignment and title matching:
- Verifies book ID matches the expected ID value
- Confirms book title matches the expected title
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for assertBook function implementation
ast-grep --pattern 'func assertBook($$$)'
Length of output: 402
clean-code/app/server/handlers/books.go (2)
1-10
: LGTM! Clean package structure and imports.
The package organization follows clean architecture principles with clear separation of concerns.
1-47
: Verify consistent error handling across handlers.
Let's ensure that the error handling pattern is consistent across all handlers in the application.
clean-code/app/server/services/books_test.go (1)
1-12
: LGTM! Clean imports and proper package organization.
clean-code/app/datasources/database/postgres_db.go (2)
3-10
: LGTM! Appropriate imports for PostgreSQL operations
The imports are well-organized and include the necessary packages from pgx v5 for PostgreSQL operations.
29-31
: LGTM! Clean type definition
The struct definition is minimal and follows interface segregation principle.
clean-code/app/datasources/database/postgres_db_test.go (1)
68-70
: Use specific database errors (similar to GetBooks_Fail)
Similar to the feedback for GetBooks_Fail, consider using specific database errors and more precise error message assertions.
Also applies to: 75-76
clean-code/app/server/handlers/books_test.go (3)
1-16
: LGTM! Well-organized imports with appropriate testing dependencies.
The imports are clean and well-structured, properly separating standard library imports from external and internal dependencies.
18-18
: LGTM! Good use of constant for API route.
Defining the route as a constant reduces duplication and makes maintenance easier.
1-102
: Verify handler implementations exist.
Let's ensure the handlers being tested are properly implemented.
✅ Verification successful
Handler implementations are properly implemented and match test coverage
The handlers GetBooks
and AddBook
are correctly implemented in clean-code/app/server/handlers/books.go
and their implementations align perfectly with the test scenarios:
GetBooks
handles successful retrieval and error cases, matching test cases inTestGetBooks
andTestGetBooks_ServiceFails
AddBook
handles successful creation, invalid requests, and service errors, matching test cases inTestAddBook
,TestAddBook_InvalidRequest
, andTestAddBook_ServiceFails
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for GetBooks and AddBook handler implementations
ast-grep --pattern 'func GetBooks($_) fiber.Handler {
$$$
}'
ast-grep --pattern 'func AddBook($_) fiber.Handler {
$$$
}'
Length of output: 4835
func TestAddBook(t *testing.T) { | ||
mockService := new(services.BooksServiceMock) | ||
mockService.On("SaveBook", mock.Anything, domain.Book{Title: "Title"}).Return(nil) | ||
|
||
app := fiber.New() | ||
app.Post(booksRoute, AddBook(mockService)) | ||
|
||
resp, err := app.Test(postRequest(booksRoute, `{"title":"Title"}`)) | ||
assert.Nil(t, err) | ||
assert.Equal(t, 201, resp.StatusCode) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add validation test cases for AddBook.
Consider adding test cases for input validation:
- Empty book title
- Very long book title
- Special characters in title
func TestAddBook_ValidationCases(t *testing.T) {
testCases := []struct {
name string
requestBody string
expectedCode int
}{
{
name: "empty title",
requestBody: `{"title":""}`,
expectedCode: 400,
},
{
name: "very long title",
requestBody: fmt.Sprintf(`{"title":"%s"}`, strings.Repeat("a", 1001)),
expectedCode: 400,
},
{
name: "title with special chars",
requestBody: `{"title":"<script>alert('xss')</script>"}`,
expectedCode: 400,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockService := new(services.BooksServiceMock)
app := fiber.New()
app.Post(booksRoute, AddBook(mockService))
resp, err := app.Test(postRequest(booksRoute, tc.requestBody))
assert.Nil(t, err)
assert.Equal(t, tc.expectedCode, resp.StatusCode)
})
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above, validation not in scope
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@norri, understood.
@@ -0,0 +1,26 @@ | |||
## Clean code example for Fiber and PostgreSQL | |||
|
|||
This is an example of a RESTful API built using the Fiber framework (https://gofiber.io/) and PostgreSQL as the database. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please describe more about the part of the clean code here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
clean-code/README.md
Outdated
@@ -0,0 +1,26 @@ | |||
## Clean code example for Fiber and PostgreSQL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls add a link in the README.md in root folder to this file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
DatabaseURL string | ||
} | ||
|
||
func NewConfiguration() *Configuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/Pungyeon/clean-go-article#Comments
can you add comments to all public vars/functions/structs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it most useful for libraries but added here also where it could be useful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Outside diff range and nitpick comments (9)
clean-code/app/datasources/database/db.go (2)
11-14
: Consider using more specific types for ID and Title fields.For better type safety and domain modeling:
ID
could be a custom type to prevent accidental mixing with other IDsTitle
could have a maximum length constraint+// BookID is a typed identifier for books +type BookID int type Book struct { - ID int + ID BookID Title string `validate:"required,max=255"` }
21-27
: Define specific error types for database operations.To improve error handling and make it easier for clients to handle specific error cases, consider defining custom error types.
+// Database errors +var ( + ErrBookNotFound = errors.New("book not found") + ErrDuplicateTitle = errors.New("book with this title already exists") + ErrInvalidBook = errors.New("invalid book data") +) type Database interface { LoadAllBooks(ctx context.Context) ([]Book, error) CreateBook(ctx context.Context, newBook NewBook) error CloseConnections() }clean-code/app/server/handlers/books.go (2)
12-25
: Add request timeout and caching headers.Consider the following improvements for better production readiness:
- Add context timeout to prevent long-running requests
- Include caching headers for GET endpoint optimization
func GetBooks(service services.BooksService) fiber.Handler { return func(c *fiber.Ctx) error { - books, err := service.GetBooks(c.UserContext()) + ctx, cancel := context.WithTimeout(c.UserContext(), 5*time.Second) + defer cancel() + + books, err := service.GetBooks(ctx) if err != nil { log.Printf("GetBooks failed: %v", err) return sendError(c, fiber.StatusInternalServerError, "internal error") } + c.Set("Cache-Control", "public, max-age=300") return c.JSON(domain.BooksResponse{ Books: books, }) } }
6-8
: Consider documenting service interface contract.Since this is part of a clean architecture example, consider adding interface documentation to clearly define the contract between layers:
- Document expected error types from the service layer
- Add comments describing the service interface contract
- Consider adding service mock examples for testing
This would make the example more valuable for developers learning clean architecture patterns.
clean-code/README.md (3)
15-19
: Consider adding specific examples of the modular structureThe clean code principles are well explained, but it would be helpful to provide specific examples of how the codebase implements these principles. Consider adding a directory structure or specific examples of the separation of concerns.
For example:
- **Modular Structure**: The code is organized into distinct modules: - `handlers/`: HTTP request handlers - `services/`: Business logic - `repositories/`: Data access layer - `models/`: Domain models
23-28
: Fix list numbering and enhance setup instructionsPlease fix the ordered list numbering and consider adding prerequisites and verification steps:
### Start +Prerequisites: +- Docker and Docker Compose installed +- Port 3000 available on your machine + 1. Build and start the containers: ```sh docker compose up --build ``` -1. The application should now be running and accessible at `http://localhost:3000`. +2. The application should now be running and accessible at `http://localhost:3000`. + +3. Verify the setup: + ```sh + curl http://localhost:3000/api/v1/books + ```
30-42
: Enhance API documentation with response examples and status codesConsider adding response examples and status codes for each endpoint:
### Endpoints - `GET /api/v1/books`: Retrieves a list of all books. +- `GET /api/v1/books`: Retrieves a list of all books. + - Response: 200 OK ```sh curl -X GET http://localhost:3000/api/v1/books
- Example response:
- {
- "books": [
{"id": 1, "title": "Title"}
- ]
- }
POST /api/v1/books
: Adds a new book to the collection.
+-POST /api/v1/books
: Adds a new book to the collection.
- Response: 201 Created
curl -X POST http://localhost:3000/api/v1/books \ -H "Content-Type: application/json" \ -d '{"title":"Title"}'
- Example response:
- {
- "id": 1,
- "title": "Title"
- }
<details> <summary>🧰 Tools</summary> <details> <summary>🪛 LanguageTool</summary> [uncategorized] ~32-~32: Loose punctuation mark. Context: ... ### Endpoints - `GET /api/v1/books`: Retrieves a list of all books. ```sh ... (UNLIKELY_OPENING_PUNCTUATION) --- [uncategorized] ~37-~37: Loose punctuation mark. Context: ...i/v1/books ``` - `POST /api/v1/books`: Adds a new book to the collection. ``... (UNLIKELY_OPENING_PUNCTUATION) </details> </details> </blockquote></details> <details> <summary>README.md (1)</summary><blockquote> `12-12`: **Fix alphabetical ordering of table of contents** The new "Clean Code" entry is not in the correct alphabetical order. It should be placed after "Clean Architecture" entry. Apply this change: ```diff - - [Clean Code](/clean-code/) - - [Clean Architecture](/clean-architecture) + - [Clean Architecture](/clean-architecture) + - [Clean Code](/clean-code/)
clean-code/app/server/handlers/books_test.go (1)
20-48
: Add test cases for edge scenarios in GetBooks.While the basic success and failure scenarios are covered, consider adding tests for:
- Empty book list response
- Multiple books in the response
- Service returning validation errors (if applicable)
func TestGetBooks_EmptyList(t *testing.T) { mockService := new(services.BooksServiceMock) mockService.On("GetBooks", mock.Anything).Return([]domain.Book{}, nil) app := fiber.New() app.Get(booksRoute, GetBooks(mockService)) resp, err := app.Test(httptest.NewRequest("GET", booksRoute, nil)) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) body := bodyFromResponse[domain.BooksResponse](t, resp) assert.Empty(t, body.Books) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (19)
README.md
(1 hunks)clean-code/README.md
(1 hunks)clean-code/app/config.go
(1 hunks)clean-code/app/datasources/data_sources.go
(1 hunks)clean-code/app/datasources/database/db.go
(1 hunks)clean-code/app/datasources/database/db_mock.go
(1 hunks)clean-code/app/datasources/database/db_test.go
(1 hunks)clean-code/app/datasources/database/memory_db.go
(1 hunks)clean-code/app/datasources/database/memory_db_test.go
(1 hunks)clean-code/app/datasources/database/postgres_db.go
(1 hunks)clean-code/app/datasources/database/postgres_db_test.go
(1 hunks)clean-code/app/main.go
(1 hunks)clean-code/app/server/domain/books.go
(1 hunks)clean-code/app/server/domain/errors.go
(1 hunks)clean-code/app/server/handlers/books.go
(1 hunks)clean-code/app/server/handlers/books_test.go
(1 hunks)clean-code/app/server/server.go
(1 hunks)clean-code/app/server/services/books.go
(1 hunks)clean-code/app/server/services/books_test.go
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
- clean-code/app/config.go
- clean-code/app/datasources/data_sources.go
- clean-code/app/datasources/database/db_mock.go
- clean-code/app/datasources/database/db_test.go
- clean-code/app/datasources/database/memory_db.go
- clean-code/app/datasources/database/memory_db_test.go
- clean-code/app/datasources/database/postgres_db_test.go
- clean-code/app/main.go
- clean-code/app/server/domain/books.go
- clean-code/app/server/domain/errors.go
- clean-code/app/server/server.go
- clean-code/app/server/services/books.go
- clean-code/app/server/services/books_test.go
🧰 Additional context used
🪛 LanguageTool
clean-code/README.md
[uncategorized] ~32-~32: Loose punctuation mark.
Context: ... ### Endpoints - GET /api/v1/books
: Retrieves a list of all books. ```sh ...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~37-~37: Loose punctuation mark.
Context: ...i/v1/books ``` - POST /api/v1/books
: Adds a new book to the collection. ``...
(UNLIKELY_OPENING_PUNCTUATION)
🪛 Markdownlint (0.35.0)
clean-code/README.md
3-3: null
Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (13)
clean-code/app/server/handlers/books.go (3)
1-11
: LGTM! Clean package structure and well-organized imports.
The package structure follows clean architecture principles with clear separation of concerns.
45-49
: LGTM! Clean error handling implementation.
The error handling utility provides a consistent way to return error responses while maintaining a clear separation between internal errors and user-facing messages.
27-43
: 🛠️ Refactor suggestion
Previous input validation concerns still apply.
Add request timeout and size limits.
Additional improvements needed:
- Add context timeout to prevent long-running requests
- Add request size limit to prevent memory exhaustion
func AddBook(service services.BooksService) fiber.Handler {
return func(c *fiber.Ctx) error {
+ // Limit request size to prevent memory issues
+ if c.Request().Header.ContentLength() > 1024*1024 { // 1MB limit
+ return sendError(c, fiber.StatusRequestEntityTooLarge, "request too large")
+ }
+
var book domain.Book
if err := c.BodyParser(&book); err != nil {
log.Printf("AddBook request parsing failed: %v", err)
return sendError(c, fiber.StatusBadRequest, "invalid request")
}
- err := service.SaveBook(c.UserContext(), book)
+ ctx, cancel := context.WithTimeout(c.UserContext(), 5*time.Second)
+ defer cancel()
+
+ err := service.SaveBook(ctx, book)
if err != nil {
log.Printf("AddBook failed: %v", err)
return sendError(c, fiber.StatusInternalServerError, "internal error")
clean-code/README.md (1)
1-4
: Format URL as markdown link and enhance introduction
Please format the URL as a proper markdown link and consider adding more context about what this example demonstrates:
## Clean code example for Fiber and PostgreSQL
-This is an example of a RESTful API built using the Fiber framework (https://gofiber.io/) and PostgreSQL as the database.
+This is an example of a RESTful API built using the [Fiber framework](https://gofiber.io/) and PostgreSQL as the database. It demonstrates how to structure a production-ready application following clean code principles and hexagonal architecture.
Also, as mentioned in the previous review, please add a link to this file from the root README.md.
🧰 Tools
🪛 Markdownlint (0.35.0)
3-3: null
Bare URL used
(MD034, no-bare-urls)
clean-code/app/server/handlers/books_test.go (5)
1-19
: LGTM! Well-organized test setup.
The imports are appropriately chosen for testing HTTP handlers, and the route constant helps maintain consistency across tests.
49-89
: Past review comment about validation test cases is still applicable.
The existing test cases for AddBook are good but incomplete. The previous suggestion to add validation test cases for empty title, very long title, and special characters remains valid.
91-95
: LGTM! Well-implemented request helper.
The postRequest helper function is clean and correctly sets the content type header.
97-103
: Past review comment about improving error handling is still applicable.
The previous suggestion to improve error handling in bodyFromResponse and ensure proper cleanup remains valid.
1-103
: Verify test coverage of handler methods.
Let's ensure all exported handler methods have corresponding test coverage.
✅ Verification successful
Let me try a different approach to find the handlers and tests.
Test coverage is complete for all handler methods
The verification shows that both exported handlers (GetBooks
and AddBook
) have comprehensive test coverage:
-
GetBooks
handler is tested by:TestGetBooks
: Happy pathTestGetBooks_ServiceFails
: Error path
-
AddBook
handler is tested by:TestAddBook
: Happy pathTestAddBook_InvalidRequest
: Invalid input pathTestAddBook_ServiceFails
: Error path
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if all exported handler methods have corresponding tests
# Find all exported handler functions
echo "Exported handler functions:"
ast-grep --pattern 'func $name($_) fiber.Handler {' | grep -v "_test.go"
# Find all test functions
echo -e "\nTest functions:"
ast-grep --pattern 'func Test$_($_ *testing.T)' clean-code/app/server/handlers/books_test.go
Length of output: 279
Script:
#!/bin/bash
# Find handler implementations
echo "Handler implementations:"
rg "func.*Handler" --type go -A 2 clean-code/app/server/handlers/
echo -e "\nTest functions in books_test.go:"
rg "^func Test" clean-code/app/server/handlers/books_test.go
Length of output: 985
clean-code/app/datasources/database/postgres_db.go (4)
12-18
: Well-defined PostgresPool
interface for enhanced testability
Defining the PostgresPool
interface facilitates mocking and improves the testability of the database layer.
36-48
: Efficient data retrieval in LoadAllBooks
using pgx.CollectRows
Great use of pgx.CollectRows
with pgx.RowToStructByName[Book]
for streamlined and efficient row scanning.
51-57
: Secure insertion in CreateBook
with parameterized queries
Using parameterized queries ensures that the code is safe from SQL injection attacks.
60-62
: Proper closure of database connection pool in CloseConnections
The CloseConnections
method correctly closes the database connection pool, ensuring that all resources are released.
- change log to slog - improve comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (5)
clean-code/app/config.go (3)
8-12
: Enhance struct and field documentation.While the struct has documentation, it would be more helpful to also document the exported fields, especially since this is an example of clean code.
// Configuration is used to store values from environment variables type Configuration struct { - Port string - DatabaseURL string + // Port specifies the HTTP server port (e.g., "3000") + Port string + // DatabaseURL contains the PostgreSQL connection string + DatabaseURL string }
14-24
: Standardize port format for consistency.Consider adding the ":" prefix to the port value to maintain consistency with Go's standard HTTP server patterns.
return &Configuration{ - Port: getEnvOrDefault("PORT", "3000"), + Port: ":" + getEnvOrDefault("PORT", "3000"), DatabaseURL: dbURL, }
26-31
: Add documentation to internal helper function.Even though this is an internal function, adding documentation would improve maintainability, especially in a clean code example.
+// getEnvOrDefault retrieves an environment variable value by key, +// returning the defaultValue if the key doesn't exist func getEnvOrDefault(key, defaultValue string) string {clean-code/app/datasources/database/db.go (2)
11-14
: Consider adding JSON tags for API serialization.Since this struct likely represents API response data, consider adding JSON tags to control field serialization.
type Book struct { - ID int - Title string + ID int `json:"id"` + Title string `json:"title"` }
21-32
: Consider enhancing error handling and cleanup.The interface is well-designed, but could benefit from these improvements:
- Consider using domain-specific errors instead of generic error
- CloseConnections might benefit from returning an error for cleanup issues
type Database interface { // LoadAllBooks retrieves all books from the database. - LoadAllBooks(ctx context.Context) ([]Book, error) + LoadAllBooks(ctx context.Context) ([]Book, *DatabaseError) // CreateBook adds a new book to the database. - CreateBook(ctx context.Context, newBook NewBook) error + CreateBook(ctx context.Context, newBook NewBook) *DatabaseError // CloseConnections closes all open connections to the database. - CloseConnections() + CloseConnections() error } +// DatabaseError represents specific database operation errors +type DatabaseError struct { + Op string // Operation that failed + Err error // Underlying error +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (3)
clean-code/app/config.go
(1 hunks)clean-code/app/datasources/database/db.go
(1 hunks)clean-code/app/server/handlers/books.go
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- clean-code/app/server/handlers/books.go
🧰 Additional context used
📓 Learnings (1)
clean-code/app/datasources/database/db.go (1)
Learnt from: norri
PR: gofiber/recipes#2701
File: clean-code/app/datasources/database/db.go:16-19
Timestamp: 2024-11-23T19:43:14.861Z
Learning: In the Go file `clean-code/app/datasources/database/db.go`, the `NewBook` struct should not include validation constraints as validation is not in scope.
🔇 Additional comments (2)
clean-code/app/config.go (1)
1-6
: LGTM! Good choice of logging package.
Using slog
for structured logging is a good practice for modern Go applications.
clean-code/app/datasources/database/db.go (1)
16-19
: LGTM! Clean and focused struct definition.
The struct follows single responsibility principle by focusing only on book creation data.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Outside diff range and nitpick comments (2)
clean-code/app/datasources/database/db.go (2)
21-32
: LGTM! Well-designed interface with proper documentation.The interface follows good practices:
- Uses context.Context for cancellation
- Clear method documentation
- Proper resource cleanup with CloseConnections
Consider adding methods for future pagination support (e.g.,
LoadBooks(ctx context.Context, limit, offset int) ([]Book, error)
) as the book collection grows.
34-51
: Consider enhancing URL validation.While the current implementation is functional, the URL validation could be more robust to prevent potential issues with malformed URLs.
Consider using the
url.Parse
function for more thorough URL validation:func NewDatabase(ctx context.Context, databaseURL string) (Database, error) { if databaseURL == "" { slog.Info("Using in-memory database implementation") return newMemoryDB(), nil } + if databaseURL != "" { + u, err := url.Parse(databaseURL) + if err != nil { + return nil, fmt.Errorf("invalid database URL: %w", err) + } + + switch u.Scheme { + case "postgres": + db, err := newPostgresDB(ctx, databaseURL) + if err != nil { + return nil, fmt.Errorf("failed to initialize PostgreSQL database connection: %w", err) + } + slog.Info("Using PostgreSQL database implementation") + return db, nil + default: + return nil, fmt.Errorf("unsupported database scheme: %s", u.Scheme) + } + } - - if strings.HasPrefix(databaseURL, "postgres://") { - db, err := newPostgresDB(ctx, databaseURL) - if err != nil { - return nil, fmt.Errorf("failed to initialize PostgreSQL database connection: %w", err) - } - slog.Info("Using PostgreSQL database implementation") - return db, nil - } - - return nil, fmt.Errorf("unsupported database URL scheme: %s", databaseURL) }Don't forget to add the "net/url" import:
import ( "context" "fmt" "log/slog" - "strings" + "net/url" )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (1)
clean-code/app/datasources/database/db.go
(1 hunks)
🧰 Additional context used
📓 Learnings (1)
clean-code/app/datasources/database/db.go (1)
Learnt from: norri
PR: gofiber/recipes#2701
File: clean-code/app/datasources/database/db.go:16-19
Timestamp: 2024-11-23T19:43:14.861Z
Learning: In the Go file `clean-code/app/datasources/database/db.go`, the `NewBook` struct should not include validation constraints as validation is not in scope.
🔇 Additional comments (2)
clean-code/app/datasources/database/db.go (2)
10-14
: LGTM! Well-structured Book type definition.
The struct is well-documented and follows Go conventions with appropriate field types.
16-19
: LGTM! Clean separation of creation concerns.
The NewBook type appropriately separates creation concerns from the main Book type, following good design practices.
@norri pls refresh with the latest changes from the master |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Outside diff range and nitpick comments (3)
clean-code/README.md (3)
11-11
: Format the Fiber framework URL as a proper markdown link.Convert the bare URL to a markdown link for better readability and adherence to markdown best practices.
-This is an example of a RESTful API built using the Fiber framework (https://gofiber.io/) and PostgreSQL as the database. +This is an example of a RESTful API built using the [Fiber framework](https://gofiber.io/) and PostgreSQL as the database.🧰 Tools
🪛 Markdownlint (0.35.0)
11-11: null
Bare URL used(MD034, no-bare-urls)
31-36
: Fix formatting and enhance setup instructions.The setup instructions could be improved in several ways:
- Fix the numbered list formatting (both items show as "1.")
- Add prerequisites section mentioning Docker requirements
- Add expected output or success indicators
## Start +### Prerequisites +- Docker and Docker Compose installed + 1. Build and start the containers: ```sh docker compose up --build ``` -1. The application should now be running and accessible at `http://localhost:3000`. +2. The application should now be running and accessible at `http://localhost:3000`. + + You should see output indicating that the server is listening and the database is connected.🧰 Tools
🪛 Markdownlint (0.35.0)
32-32: null
Fenced code blocks should be surrounded by blank lines(MD031, blanks-around-fences)
38-50
: Enhance API documentation with more details.While the current documentation is clear, it could be more helpful with:
- Response examples
- Expected status codes
- Error scenarios
## Endpoints - `GET /api/v1/books`: Retrieves a list of all books. + - Success Response (200 OK): ```sh curl -X GET http://localhost:3000/api/v1/books ``` + ```json + { + "books": [ + {"id": 1, "title": "Example Book"} + ] + } + ``` - `POST /api/v1/books`: Adds a new book to the collection. + - Request Body: + - `title` (string, required): The title of the book + - Success Response (201 Created): ```sh curl -X POST http://localhost:3000/api/v1/books \ -H "Content-Type: application/json" \ -d '{"title":"Title"}' ``` + ```json + { + "id": 1, + "title": "Title" + } + ``` + - Error Response (400 Bad Request): + - When title is missing or invalid🧰 Tools
🪛 LanguageTool
[uncategorized] ~40-~40: Loose punctuation mark.
Context: ... ## Endpoints -GET /api/v1/books
: Retrieves a list of all books. ```sh ...(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~45-~45: Loose punctuation mark.
Context: ...i/v1/books ``` -POST /api/v1/books
: Adds a new book to the collection. ``...(UNLIKELY_OPENING_PUNCTUATION)
🪛 Markdownlint (0.35.0)
41-41: null
Fenced code blocks should be surrounded by blank lines(MD031, blanks-around-fences)
46-46: null
Fenced code blocks should be surrounded by blank lines(MD031, blanks-around-fences)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (1)
clean-code/README.md
(1 hunks)
🧰 Additional context used
🪛 LanguageTool
clean-code/README.md
[uncategorized] ~40-~40: Loose punctuation mark.
Context: ... ## Endpoints - GET /api/v1/books
: Retrieves a list of all books. ```sh ...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~45-~45: Loose punctuation mark.
Context: ...i/v1/books ``` - POST /api/v1/books
: Adds a new book to the collection. ``...
(UNLIKELY_OPENING_PUNCTUATION)
🪛 Markdownlint (0.35.0)
clean-code/README.md
37-37: Expected: 0 or 2; Actual: 3
Trailing spaces
(MD009, no-trailing-spaces)
7-7: null
Multiple top-level headings in the same document
(MD025, single-title, single-h1)
32-32: null
Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
41-41: null
Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
46-46: null
Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
11-11: null
Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (2)
clean-code/README.md (2)
1-10
: LGTM! Well-structured metadata and navigation.
The frontmatter provides clear SEO information, and the badges offer convenient access to both the source code and live environment.
🧰 Tools
🪛 Markdownlint (0.35.0)
7-7: null
Multiple top-level headings in the same document
(MD025, single-title, single-h1)
13-28
: LGTM! Excellent explanation of clean code principles.
The description effectively communicates both the general principles of clean code and their specific application in this example. This addresses the previous review comment requesting more detailed description.
This PR provides alternative clean code version to clean-architecture and hexagonal recipes. This includes:
pgx
library can be used and unit testedSummary by CodeRabbit
Release Notes
New Features
Documentation
Tests
Chores
.gitignore
entry fordb_data
to prevent tracking of database-related files.