-
-
Notifications
You must be signed in to change notification settings - Fork 457
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add clean-code example with postgres
- Loading branch information
Showing
27 changed files
with
910 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
db_data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
FROM golang:1.23 | ||
RUN apt update && apt upgrade -y && apt install -y git | ||
|
||
WORKDIR /go/src/app | ||
COPY app ./ | ||
RUN go mod tidy && go mod verify | ||
|
||
ENTRYPOINT [ "go", "run", "." ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
||
### Start | ||
|
||
1. Build and start the containers: | ||
```sh | ||
docker compose up --build | ||
``` | ||
|
||
1. The application should now be running and accessible at `http://localhost:3000`. | ||
|
||
### Endpoints | ||
|
||
- `GET /api/v1/books`: Retrieves a list of all books. | ||
```sh | ||
curl -X GET http://localhost:3000/api/v1/books | ||
``` | ||
|
||
- `POST /api/v1/books`: Adds a new book to the collection. | ||
```sh | ||
curl -X POST http://localhost:3000/api/v1/books \ | ||
-H "Content-Type: application/json" \ | ||
-d '{"title":"Title"}' | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package main | ||
|
||
import "os" | ||
|
||
type Configuration struct { | ||
Port string | ||
DatabaseURL string | ||
} | ||
|
||
func NewConfiguration() *Configuration { | ||
return &Configuration{ | ||
Port: getEnvOrDefault("PORT", "3000"), | ||
DatabaseURL: getEnvOrDefault("DATABASE_URL", ""), | ||
} | ||
} | ||
|
||
func getEnvOrDefault(key, defaultValue string) string { | ||
if value, exists := os.LookupEnv(key); exists { | ||
return value | ||
} | ||
return defaultValue | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package main | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestNewConfiguration(t *testing.T) { | ||
os.Setenv("PORT", "8080") | ||
os.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/dbname") | ||
defer os.Unsetenv("PORT") | ||
defer os.Unsetenv("DATABASE_URL") | ||
|
||
conf := NewConfiguration() | ||
|
||
assert.Equal(t, "8080", conf.Port) | ||
assert.Equal(t, "postgres://user:pass@localhost:5432/dbname", conf.DatabaseURL) | ||
} | ||
|
||
func TestNewConfiguration_Defaults(t *testing.T) { | ||
os.Unsetenv("PORT") | ||
os.Unsetenv("DATABASE_URL") | ||
|
||
conf := NewConfiguration() | ||
|
||
assert.Equal(t, "3000", conf.Port) | ||
assert.Equal(t, "", conf.DatabaseURL) | ||
} | ||
|
||
func TestGetEnvOrDefault(t *testing.T) { | ||
os.Setenv("TEST_ENV", "value") | ||
defer os.Unsetenv("TEST_ENV") | ||
|
||
value := getEnvOrDefault("TEST_ENV", "default") | ||
assert.Equal(t, "value", value) | ||
|
||
value = getEnvOrDefault("NON_EXISTENT_ENV", "default") | ||
assert.Equal(t, "default", value) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package datasources | ||
|
||
import "app/datasources/database" | ||
|
||
type DataSources struct { | ||
DB database.Database | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package database | ||
|
||
import ( | ||
"context" | ||
"log" | ||
"strings" | ||
) | ||
|
||
type Book struct { | ||
ID int | ||
Title string | ||
} | ||
|
||
type Database interface { | ||
LoadAllBooks(ctx context.Context) ([]Book, error) | ||
CreateBook(ctx context.Context, newBook Book) error | ||
CloseConnections() | ||
} | ||
|
||
func NewDatabase(ctx context.Context, databaseURL string) Database { | ||
if databaseURL == "" { | ||
log.Printf("Using in-memory database") | ||
return newMemoryDB() | ||
} else if strings.HasPrefix(databaseURL, "postgres://") { | ||
db, err := newPostgresDB(ctx, databaseURL) | ||
if err != nil { | ||
log.Panicf("failed to create postgres database: %v", err) | ||
} | ||
log.Printf("Using Postgres database") | ||
return db | ||
} | ||
log.Panicf("unsupported database: %s", databaseURL) | ||
return nil | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package database | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
type DatabaseMock struct { | ||
mock.Mock | ||
} | ||
|
||
func (m *DatabaseMock) LoadAllBooks(ctx context.Context) ([]Book, error) { | ||
args := m.Called(ctx) | ||
if args.Get(0) == nil { | ||
return nil, args.Error(1) | ||
} | ||
return args.Get(0).([]Book), args.Error(1) | ||
} | ||
|
||
func (m *DatabaseMock) CreateBook(ctx context.Context, newBook Book) error { | ||
args := m.Called(ctx, newBook) | ||
return args.Error(0) | ||
} | ||
|
||
func (m *DatabaseMock) CloseConnections() { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package database | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestNewDatabase_MemoryDB(t *testing.T) { | ||
ctx := context.Background() | ||
db := NewDatabase(ctx, "") | ||
assert.Equal(t, "*database.memoryDB", reflect.TypeOf(db).String()) | ||
} | ||
|
||
func TestNewDatabase_PostgresDB(t *testing.T) { | ||
ctx := context.Background() | ||
db := NewDatabase(ctx, "postgres://localhost:5432") | ||
assert.Equal(t, "*database.postgresDB", reflect.TypeOf(db).String()) | ||
} | ||
|
||
func TestNewDatabase_InvalidDatabaseConfiguration(t *testing.T) { | ||
ctx := context.Background() | ||
defer func() { | ||
assert.NotNil(t, recover()) | ||
}() | ||
_ = NewDatabase(ctx, "invalid") | ||
} | ||
|
||
func assertBook(t *testing.T, book Book, expectedID int, expected Book) { | ||
assert.Equal(t, expectedID, book.ID) | ||
assert.Equal(t, expected.Title, book.Title) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package database | ||
|
||
import "context" | ||
|
||
// This is just an example and not for production use | ||
func newMemoryDB() Database { | ||
return &memoryDB{ | ||
records: make([]Book, 0, 10), | ||
idCounter: 0, | ||
} | ||
} | ||
|
||
type memoryDB struct { | ||
records []Book | ||
idCounter int | ||
} | ||
|
||
func (db *memoryDB) LoadAllBooks(_ context.Context) ([]Book, error) { | ||
return db.records, nil | ||
} | ||
|
||
func (db *memoryDB) CreateBook(_ context.Context, newBook Book) error { | ||
db.records = append(db.records, Book{ | ||
ID: db.idCounter, | ||
Title: newBook.Title, | ||
}) | ||
db.idCounter++ | ||
return nil | ||
} | ||
|
||
func (db *memoryDB) CloseConnections() { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package database | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestMemoryDB_LoadBooks(t *testing.T) { | ||
db := newMemoryDB() | ||
books, err := db.LoadAllBooks(context.Background()) | ||
assert.Nil(t, err) | ||
assert.Equal(t, 0, len(books)) | ||
} | ||
|
||
func TestMemoryDB_SaveBook(t *testing.T) { | ||
db := newMemoryDB() | ||
newBook := Book{Title: "Title"} | ||
err := db.CreateBook(context.Background(), newBook) | ||
assert.Nil(t, err) | ||
|
||
books, err := db.LoadAllBooks(context.Background()) | ||
assert.Nil(t, err) | ||
assert.Equal(t, 1, len(books)) | ||
assertBook(t, books[0], 0, newBook) | ||
} | ||
|
||
func TestMemoryDB_SaveBookMultiple(t *testing.T) { | ||
db := newMemoryDB() | ||
newBook1 := Book{Title: "Title1"} | ||
err := db.CreateBook(context.Background(), newBook1) | ||
assert.Nil(t, err) | ||
|
||
newBook2 := Book{Title: "Title2"} | ||
err = db.CreateBook(context.Background(), newBook2) | ||
assert.Nil(t, err) | ||
|
||
books, err := db.LoadAllBooks(context.Background()) | ||
assert.Nil(t, err) | ||
assert.Equal(t, 2, len(books)) | ||
assertBook(t, books[0], 0, newBook1) | ||
assertBook(t, books[1], 1, newBook2) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package database | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/jackc/pgx/v5" | ||
"github.com/jackc/pgx/v5/pgconn" | ||
"github.com/jackc/pgx/v5/pgxpool" | ||
) | ||
|
||
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() | ||
} | ||
|
||
func newPostgresDB(ctx context.Context, databaseURL string) (Database, error) { | ||
dbpool, err := pgxpool.New(ctx, databaseURL) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to create connection pool: %v", err) | ||
} | ||
|
||
return &postgresDB{ | ||
pool: dbpool, | ||
}, nil | ||
} | ||
|
||
type postgresDB struct { | ||
pool PostgresPool | ||
} | ||
|
||
func (db *postgresDB) LoadAllBooks(ctx context.Context) ([]Book, error) { | ||
rows, err := db.pool.Query(ctx, "SELECT id, title FROM books") | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to query books table: %w", err) | ||
} | ||
defer rows.Close() | ||
|
||
var books []Book | ||
for rows.Next() { | ||
var record Book | ||
err := rows.Scan(&record.ID, &record.Title) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to scan rows: %w", err) | ||
} | ||
books = append(books, record) | ||
} | ||
return books, nil | ||
} | ||
|
||
func (db *postgresDB) CreateBook(ctx context.Context, newBook Book) error { | ||
_, err := db.pool.Exec(ctx, "INSERT INTO books (title) VALUES ($1)", newBook.Title) | ||
if err != nil { | ||
return fmt.Errorf("failed to insert book: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (db *postgresDB) CloseConnections() { | ||
db.pool.Close() | ||
} |
Oops, something went wrong.