Skip to content

Commit

Permalink
add clean-code example with postgres
Browse files Browse the repository at this point in the history
  • Loading branch information
norri committed Nov 22, 2024
1 parent 1c6adcc commit 46f9dc9
Show file tree
Hide file tree
Showing 27 changed files with 910 additions and 0 deletions.
1 change: 1 addition & 0 deletions clean-code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
db_data
8 changes: 8 additions & 0 deletions clean-code/Dockerfile-local
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", "." ]
26 changes: 26 additions & 0 deletions clean-code/README.md
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"}'
```
22 changes: 22 additions & 0 deletions clean-code/app/config.go
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
}
41 changes: 41 additions & 0 deletions clean-code/app/config_test.go
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)
}
7 changes: 7 additions & 0 deletions clean-code/app/datasources/data_sources.go
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
}
35 changes: 35 additions & 0 deletions clean-code/app/datasources/database/db.go
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

}
27 changes: 27 additions & 0 deletions clean-code/app/datasources/database/db_mock.go
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() {
}
34 changes: 34 additions & 0 deletions clean-code/app/datasources/database/db_test.go
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)
}
32 changes: 32 additions & 0 deletions clean-code/app/datasources/database/memory_db.go
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() {
}
44 changes: 44 additions & 0 deletions clean-code/app/datasources/database/memory_db_test.go
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)
}
62 changes: 62 additions & 0 deletions clean-code/app/datasources/database/postgres_db.go
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()
}
Loading

0 comments on commit 46f9dc9

Please sign in to comment.