Skip to content

Commit

Permalink
🔥 add viper, cobra and continue post examples
Browse files Browse the repository at this point in the history
  • Loading branch information
lfcifuentes committed Jun 1, 2024
1 parent 415851a commit bd20fca
Show file tree
Hide file tree
Showing 18 changed files with 1,005 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MONGO_URI=mongodb://localhost:27017
MONGO_URI_TEST=mongodb://localhost:27017
MONGO_DB=databasename
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
*.out
*.out
*.env
52 changes: 52 additions & 0 deletions aggregate/product.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package aggregate

import (
"errors"

"github.com/google/uuid"
"github.com/lfcifuentes/ddd-go/entity"
)

var (
// ErrMissingValues is returned when a product is created without a name or description
ErrMissingValues = errors.New("missing values")
)

// Product is a aggregate that combines item with a price and quantity
type Product struct {
// item is the root entity which is an item
item *entity.Item
price float64
// Quantity is the number of products in stock
quantity int
}

// NewProduct will create a new product
// will return error if name of description is empty
func NewProduct(name, description string, price float64) (Product, error) {
if name == "" || description == "" {
return Product{}, ErrMissingValues
}

return Product{
item: &entity.Item{
ID: uuid.New(),
Name: name,
Description: description,
},
price: price,
quantity: 0,
}, nil
}

func (p Product) GetID() uuid.UUID {
return p.item.ID
}

func (p Product) GetItem() *entity.Item {
return p.item
}

func (p Product) GetPrice() float64 {
return p.price
}
39 changes: 39 additions & 0 deletions aggregate/product_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package aggregate

import (
"testing"
)

func TestProduct_NewProduct(t *testing.T) {
type testCase struct {
test string
name string
description string
price float64
expectedErr error
}

testCases := []testCase{
{
test: "should return error if name is empty",
name: "",
expectedErr: ErrMissingValues,
},
{
test: "validvalues",
name: "test",
description: "test",
price: 1.0,
expectedErr: nil,
},
}

for _, tc := range testCases {
t.Run(tc.test, func(t *testing.T) {
_, err := NewProduct(tc.name, tc.description, tc.price)
if err != tc.expectedErr {
t.Errorf("Expected error: %v, got: %v", tc.expectedErr, err)
}
})
}
}
23 changes: 23 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "ddd-go",
Short: "Implementation of DDD in Go",
Long: ``,
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
90 changes: 90 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cmd

import (
"fmt"

"github.com/google/uuid"
"github.com/lfcifuentes/ddd-go/aggregate"
"github.com/lfcifuentes/ddd-go/services/order"
"github.com/lfcifuentes/ddd-go/services/tavern"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func init() {
rootCmd.AddCommand(serveCmd)
}

// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts the server",
Long: ``,
Run: startServer,
}

func startServer(_ *cobra.Command, _ []string) {
// create products array
products := CreateProducts()

fmt.Println("Products:")
for _, p := range products {
fmt.Printf("Name: %s, Description: %s, Price: %f\n", p.GetItem().Name, p.GetItem().Description, p.GetPrice())
}

// Create Order Service to use in tavern
os, err := order.NewOrderService(
order.WithMongoCustomerRepository(viper.GetString("MONGO_URI")),
order.WithMemoryProductRepository(products),
)
if err != nil {
fmt.Println("Error creating order service")
panic(err)
}

// Create tavern service
tavern, err := tavern.NewTavern(tavern.WithOrderService(os))
if err != nil {
fmt.Println("Error creating tavern service")
panic(err)
}

cus, err := os.AddCustomer("Percy")
if err != nil {
fmt.Println("Error creating customer")
panic(err)
}

order := []uuid.UUID{
products[0].GetID(),
}

// Execute Order
err = tavern.Order(cus, order)
if err != nil {
panic(err)
}
}

func CreateProducts() []aggregate.Product {
apple, err := aggregate.NewProduct("Apple", "Red apple", 0.5)
if err != nil {
panic(err)
}

banana, err := aggregate.NewProduct("Banana", "Yellow banana", 0.3)
if err != nil {
panic(err)
}

orage, err := aggregate.NewProduct("Orange", "Orange orange", 0.7)
if err != nil {
panic(err)
}

return []aggregate.Product{
apple,
banana,
orage,
}
}
96 changes: 96 additions & 0 deletions domain/customer/mongo/mongo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Mongo is a mongo implementation of the Customer Repository
package mongo

import (
"context"
"time"

"github.com/google/uuid"
"github.com/lfcifuentes/ddd-go/aggregate"
"github.com/spf13/viper"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

type MongoRepository struct {
db *mongo.Database
// customer is used to store customers
customer *mongo.Collection
}

// mongoCustomer is an internal type that is used to store a CustomerAggregate
// we make an internal struct for this to avoid coupling this mongo implementation to the customeraggregate.
// Mongo uses bson so we add tags for that
type mongoCustomer struct {
ID uuid.UUID `bson:"id"`
Name string `bson:"name"`
}

// NewFromCustomer takes in a aggregate and converts into internal structure
func NewFromCustomer(c aggregate.Customer) mongoCustomer {
return mongoCustomer{
ID: c.GetID(),
Name: c.GetName(),
}
}

// ToAggregate converts into a aggregate.Customer
// this could validate all values present etc
func (m mongoCustomer) ToAggregate() aggregate.Customer {
c := aggregate.Customer{}

c.SetID(m.ID)
c.SetName(m.Name)

return c

}

// Create a new mongodb repository
func New(ctx context.Context, connectionString string) (*MongoRepository, error) {
client, err := mongo.Connect(ctx, options.Client().ApplyURI(connectionString))
if err != nil {
return nil, err
}

// Find Metabot DB
db := client.Database(viper.GetString("MONGO_DB"))
customers := db.Collection("customers")

return &MongoRepository{
db: db,
customer: customers,
}, nil
}

func (mr *MongoRepository) Get(id uuid.UUID) (aggregate.Customer, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

result := mr.customer.FindOne(ctx, bson.M{"id": id})

var c mongoCustomer
err := result.Decode(&c)
if err != nil {
return aggregate.Customer{}, err
}
// Convert to aggregate
return c.ToAggregate(), nil
}

func (mr *MongoRepository) Add(c aggregate.Customer) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

internal := NewFromCustomer(c)
_, err := mr.customer.InsertOne(ctx, internal)
if err != nil {
return err
}
return nil
}

func (mr *MongoRepository) Update(c aggregate.Customer) error {
panic("to implement")
}
76 changes: 76 additions & 0 deletions domain/product/memory/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package memory

import (
"sync"

"github.com/google/uuid"
"github.com/lfcifuentes/ddd-go/aggregate"
"github.com/lfcifuentes/ddd-go/domain/product"
)

type MemoryProductRepository struct {
products map[uuid.UUID]aggregate.Product
sync.Mutex
}

// New is a factory function to generate a new repository of products
func New() *MemoryProductRepository {
return &MemoryProductRepository{
products: make(map[uuid.UUID]aggregate.Product),
}
}

func (mr *MemoryProductRepository) GetAll() ([]aggregate.Product, error) {
products := make([]aggregate.Product, 0, len(mr.products))
for _, product := range mr.products {
products = append(products, product)
}
return products, nil
}

// GetByID searches for a product based on it's ID
func (mpr *MemoryProductRepository) GetByID(id uuid.UUID) (aggregate.Product, error) {
if product, ok := mpr.products[uuid.UUID(id)]; ok {
return product, nil
}
return aggregate.Product{}, product.ErrProductNotFound
}

// Add will add a new product to the repository
func (mpr *MemoryProductRepository) Add(newprod aggregate.Product) error {
mpr.Lock()
defer mpr.Unlock()

if _, ok := mpr.products[newprod.GetID()]; ok {
return product.ErrProductAlreadyExist
}

mpr.products[newprod.GetID()] = newprod

return nil
}

// Update will change all values for a product based on it's ID
func (mpr *MemoryProductRepository) Update(upprod aggregate.Product) error {
mpr.Lock()
defer mpr.Unlock()

if _, ok := mpr.products[upprod.GetID()]; !ok {
return product.ErrProductNotFound
}

mpr.products[upprod.GetID()] = upprod
return nil
}

// Delete remove an product from the repository
func (mpr *MemoryProductRepository) Delete(id uuid.UUID) error {
mpr.Lock()
defer mpr.Unlock()

if _, ok := mpr.products[id]; !ok {
return product.ErrProductNotFound
}
delete(mpr.products, id)
return nil
}
Loading

0 comments on commit bd20fca

Please sign in to comment.