diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d398103 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +MONGO_URI=mongodb://localhost:27017 +MONGO_URI_TEST=mongodb://localhost:27017 +MONGO_DB=databasename \ No newline at end of file diff --git a/.gitignore b/.gitignore index ef120a0..dbb510f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -*.out \ No newline at end of file +*.out +*.env \ No newline at end of file diff --git a/aggregate/product.go b/aggregate/product.go new file mode 100644 index 0000000..1d06ad8 --- /dev/null +++ b/aggregate/product.go @@ -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 +} diff --git a/aggregate/product_test.go b/aggregate/product_test.go new file mode 100644 index 0000000..923022c --- /dev/null +++ b/aggregate/product_test.go @@ -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) + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..b41fd19 --- /dev/null +++ b/cmd/root.go @@ -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) + } +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..074386d --- /dev/null +++ b/cmd/serve.go @@ -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, + } +} diff --git a/domain/customer/mongo/mongo.go b/domain/customer/mongo/mongo.go new file mode 100644 index 0000000..1f6c0a2 --- /dev/null +++ b/domain/customer/mongo/mongo.go @@ -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") +} diff --git a/domain/product/memory/memory.go b/domain/product/memory/memory.go new file mode 100644 index 0000000..035c561 --- /dev/null +++ b/domain/product/memory/memory.go @@ -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 +} diff --git a/domain/product/memory/memory_test.go b/domain/product/memory/memory_test.go new file mode 100644 index 0000000..1882ab1 --- /dev/null +++ b/domain/product/memory/memory_test.go @@ -0,0 +1,84 @@ +package memory + +import ( + "testing" + + "github.com/google/uuid" + "github.com/lfcifuentes/ddd-go/aggregate" + "github.com/lfcifuentes/ddd-go/domain/product" +) + +func TestMemoryProductRepository_Add(t *testing.T) { + repo := New() + product, err := aggregate.NewProduct("Beer", "Good for you're health", 1.99) + if err != nil { + t.Error(err) + } + + repo.Add(product) + if len(repo.products) != 1 { + t.Errorf("Expected 1 product, got %d", len(repo.products)) + } +} + +func TestMemoryProductRepository_Get(t *testing.T) { + repo := New() + existingProd, err := aggregate.NewProduct("Beer", "Good for you're health", 1.99) + if err != nil { + t.Error(err) + } + + repo.Add(existingProd) + if len(repo.products) != 1 { + t.Errorf("Expected 1 product, got %d", len(repo.products)) + } + + type testCase struct { + name string + id uuid.UUID + expectedErr error + } + + testCases := []testCase{ + { + name: "Get product by id", + id: existingProd.GetID(), + expectedErr: nil, + }, { + name: "Get non-existing product by id", + id: uuid.New(), + expectedErr: product.ErrProductNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := repo.GetByID(tc.id) + if err != tc.expectedErr { + t.Errorf("Expected error %v, got %v", tc.expectedErr, err) + } + + }) + } +} + +func TestMemoryProductRepository_Delete(t *testing.T) { + repo := New() + existingProd, err := aggregate.NewProduct("Beer", "Good for you're health", 1.99) + if err != nil { + t.Error(err) + } + + repo.Add(existingProd) + if len(repo.products) != 1 { + t.Errorf("Expected 1 product, got %d", len(repo.products)) + } + + err = repo.Delete(existingProd.GetID()) + if err != nil { + t.Error(err) + } + if len(repo.products) != 0 { + t.Errorf("Expected 0 products, got %d", len(repo.products)) + } +} diff --git a/domain/product/repository.go b/domain/product/repository.go new file mode 100644 index 0000000..c255b69 --- /dev/null +++ b/domain/product/repository.go @@ -0,0 +1,24 @@ +package product + +import ( + "errors" + + "github.com/google/uuid" + "github.com/lfcifuentes/ddd-go/aggregate" +) + +var ( + //ErrProductNotFound is returned when a product is not found + ErrProductNotFound = errors.New("the product was not found") + //ErrProductAlreadyExist is returned when trying to add a product that already exists + ErrProductAlreadyExist = errors.New("the product already exists") +) + +// ProductRepository is the repository interface to fulfill to use the product aggregate +type ProductRepository interface { + GetAll() ([]aggregate.Product, error) + GetByID(id uuid.UUID) (aggregate.Product, error) + Add(product aggregate.Product) error + Update(product aggregate.Product) error + Delete(id uuid.UUID) error +} diff --git a/go.mod b/go.mod index f9fac27..50e2d32 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,44 @@ module github.com/lfcifuentes/ddd-go go 1.19 require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.9.0 + go.mongodb.org/mongo-driver v1.15.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a571806..5d322de 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,118 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d0f964d --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + + "github.com/joho/godotenv" + "github.com/lfcifuentes/ddd-go/cmd" + "github.com/spf13/viper" +) + +func main() { + // Load .env file + err := godotenv.Load() + if err != nil { + log.Fatalf("Error loading .env file: %s", err) + } + + // Bind environment variables + viper.AutomaticEnv() + + // Execute root command + cmd.Execute() +} diff --git a/readme.md b/readme.md index 4cc8707..827d6d8 100644 --- a/readme.md +++ b/readme.md @@ -59,4 +59,11 @@ go test ./... -coverprofile=coverage.out ``` ```bash go tool cover -html=coverage.out +``` + +#### Run Server + +Run development server +```bash +go run main.go serve ``` \ No newline at end of file diff --git a/services/order/order.go b/services/order/order.go new file mode 100644 index 0000000..e517908 --- /dev/null +++ b/services/order/order.go @@ -0,0 +1,127 @@ +package order + +import ( + "context" + "log" + + "github.com/google/uuid" + "github.com/lfcifuentes/ddd-go/aggregate" + "github.com/lfcifuentes/ddd-go/domain/customer" + "github.com/lfcifuentes/ddd-go/domain/customer/memory" + "github.com/lfcifuentes/ddd-go/domain/customer/mongo" + "github.com/lfcifuentes/ddd-go/domain/product" + prodmemory "github.com/lfcifuentes/ddd-go/domain/product/memory" +) + +// OrderConfiguration is an alias for a function that will take in a pointer to an OrderService and modify it +type OrderConfiguration func(os *OrderService) error + +// OrderService is a implementation of the OrderService +type OrderService struct { + customers customer.CustomerRepository + products product.ProductRepository +} + +// NewOrderService takes a variable amount of OrderConfiguration functions and returns a new OrderService +// Each OrderConfiguration will be called in the order they are passed in +func NewOrderService(cfgs ...OrderConfiguration) (*OrderService, error) { + // Create the orderservice + os := &OrderService{} + // Apply all Configurations passed in + for _, cfg := range cfgs { + // Pass the service into the configuration function + err := cfg(os) + if err != nil { + return nil, err + } + } + return os, nil +} + +// WithCustomerRepository applies a given customer repository to the OrderService +func WithCustomerRepository(cr customer.CustomerRepository) OrderConfiguration { + // return a function that matches the OrderConfiguration alias, + // You need to return this so that the parent function can take in all the needed parameters + return func(os *OrderService) error { + os.customers = cr + return nil + } +} + +// WithMemoryCustomerRepository applies a memory customer repository to the OrderService +func WithMemoryCustomerRepository() OrderConfiguration { + // Create the memory repo, if we needed parameters, such as connection strings they could be inputted here + cr := memory.New() + return WithCustomerRepository(cr) +} + +// CreateOrder will chaintogether all repositories to create a order for a customer +// will return the collected price of all Products +func (o *OrderService) CreateOrder(customerID uuid.UUID, productIDs []uuid.UUID) (float64, error) { + // Get the customer + c, err := o.customers.Get(customerID) + if err != nil { + return 0, err + } + + // Get each Product, Ouchie, We need a ProductRepository + var products []aggregate.Product + var price float64 + for _, id := range productIDs { + p, err := o.products.GetByID(id) + if err != nil { + return 0, err + } + products = append(products, p) + price += p.GetPrice() + } + + // All Products exists in store, now we can create the order + log.Printf("Customer: %s has ordered %d products", c.GetID(), len(products)) + + return price, nil +} + +// AddCustomer will add a new customer and return the customerID +func (o *OrderService) AddCustomer(name string) (uuid.UUID, error) { + c, err := aggregate.NewCustomer(name) + if err != nil { + return uuid.Nil, err + } + // Add to Repo + err = o.customers.Add(c) + if err != nil { + return uuid.Nil, err + } + + return c.GetID(), nil +} + +// WithMemoryProductRepository adds a in memory product repo and adds all input products +func WithMemoryProductRepository(products []aggregate.Product) OrderConfiguration { + return func(os *OrderService) error { + // Create the memory repo, if we needed parameters, such as connection strings they could be inputted here + pr := prodmemory.New() + + // Add Items to repo + for _, p := range products { + err := pr.Add(p) + if err != nil { + return err + } + } + os.products = pr + return nil + } +} +func WithMongoCustomerRepository(connectionString string) OrderConfiguration { + return func(os *OrderService) error { + // Create the mongo repo, if we needed parameters, such as connection strings they could be inputted here + cr, err := mongo.New(context.Background(), connectionString) + if err != nil { + return err + } + os.customers = cr + return nil + } +} diff --git a/services/order/order_test.go b/services/order/order_test.go new file mode 100644 index 0000000..a8538a2 --- /dev/null +++ b/services/order/order_test.go @@ -0,0 +1,63 @@ +package order + +import ( + "testing" + + "github.com/google/uuid" + "github.com/lfcifuentes/ddd-go/aggregate" +) + +func init_products(t *testing.T) []aggregate.Product { + beer, err := aggregate.NewProduct("Beer", "Healthy Beverage", 1.99) + if err != nil { + t.Error(err) + } + peenuts, err := aggregate.NewProduct("Peenuts", "Healthy Snacks", 0.99) + if err != nil { + t.Error(err) + } + wine, err := aggregate.NewProduct("Wine", "Healthy Snacks", 0.99) + if err != nil { + t.Error(err) + } + products := []aggregate.Product{ + beer, peenuts, wine, + } + return products +} +func TestOrder_NewOrderService(t *testing.T) { + // Create a few products to insert into in memory repo + products := init_products(t) + + os, err := NewOrderService( + WithMemoryCustomerRepository(), + WithMemoryProductRepository(products), + ) + + if err != nil { + t.Error(err) + } + + // Add Customer + cust, err := aggregate.NewCustomer("Luis") + if err != nil { + t.Error(err) + } + + err = os.customers.Add(cust) + if err != nil { + t.Error(err) + } + + // Perform Order for one beer + order := []uuid.UUID{ + products[0].GetID(), + } + + _, err = os.CreateOrder(cust.GetID(), order) + + if err != nil { + t.Error(err) + } + +} diff --git a/services/tavern/tavern.go b/services/tavern/tavern.go new file mode 100644 index 0000000..7115490 --- /dev/null +++ b/services/tavern/tavern.go @@ -0,0 +1,56 @@ +package tavern + +import ( + "log" + + "github.com/google/uuid" + "github.com/lfcifuentes/ddd-go/services/order" +) + +// TavernConfiguration is an alias that takes a pointer and modifies the Tavern +type TavernConfiguration func(os *Tavern) error + +type Tavern struct { + // orderservice is used to handle orders + OrderService *order.OrderService + // BillingService is used to handle billing + // This is up to you to implement + BillingService interface{} +} + +// NewTavern takes a variable amount of TavernConfigurations and builds a Tavern +func NewTavern(cfgs ...TavernConfiguration) (*Tavern, error) { + // Create the Tavern + t := &Tavern{} + // Apply all Configurations passed in + for _, cfg := range cfgs { + // Pass the service into the configuration function + err := cfg(t) + if err != nil { + return nil, err + } + } + return t, nil +} + +// WithOrderService applies a given OrderService to the Tavern +func WithOrderService(os *order.OrderService) TavernConfiguration { + // return a function that matches the TavernConfiguration signature + return func(t *Tavern) error { + t.OrderService = os + return nil + } +} + +// Order performs an order for a customer +func (t *Tavern) Order(customer uuid.UUID, products []uuid.UUID) error { + price, err := t.OrderService.CreateOrder(customer, products) + if err != nil { + return err + } + log.Printf("Bill the Customer: %0.0f", price) + + // Bill the customer + //err = t.BillingService.Bill(customer, price) + return nil +} diff --git a/services/tavern/tavern_test.go b/services/tavern/tavern_test.go new file mode 100644 index 0000000..27c6c90 --- /dev/null +++ b/services/tavern/tavern_test.go @@ -0,0 +1,92 @@ +package tavern + +import ( + "testing" + + "github.com/google/uuid" + "github.com/lfcifuentes/ddd-go/aggregate" + "github.com/lfcifuentes/ddd-go/services/order" + "github.com/spf13/viper" +) + +func init_products(t *testing.T) []aggregate.Product { + beer, err := aggregate.NewProduct("Beer", "Healthy Beverage", 1.99) + if err != nil { + t.Error(err) + } + peenuts, err := aggregate.NewProduct("Peenuts", "Healthy Snacks", 0.99) + if err != nil { + t.Error(err) + } + wine, err := aggregate.NewProduct("Wine", "Healthy Snacks", 0.99) + if err != nil { + t.Error(err) + } + products := []aggregate.Product{ + beer, peenuts, wine, + } + return products +} +func Test_Tavern(t *testing.T) { + // Create OrderService + products := init_products(t) + + os, err := order.NewOrderService( + order.WithMongoCustomerRepository(viper.GetString("MONGO_URI_TEST")), + order.WithMemoryProductRepository(products), + ) + if err != nil { + t.Error(err) + } + + tavern, err := NewTavern(WithOrderService(os)) + if err != nil { + t.Error(err) + } + + uid, err := os.AddCustomer("LUIS") + if err != nil { + t.Error(err) + } + order := []uuid.UUID{ + products[0].GetID(), + } + // Execute Order + err = tavern.Order(uid, order) + if err != nil { + t.Error(err) + } + +} + +func Test_MongoTavern(t *testing.T) { + // Create OrderService + products := init_products(t) + + os, err := order.NewOrderService( + order.WithMongoCustomerRepository(viper.GetString("MONGO_URI_TEST")), + order.WithMemoryProductRepository(products), + ) + if err != nil { + t.Error(err) + } + + tavern, err := NewTavern(WithOrderService(os)) + if err != nil { + t.Error(err) + } + + uid, err := os.AddCustomer("LUIS") + if err != nil { + t.Error(err) + } + order := []uuid.UUID{ + products[0].GetID(), + } + // Execute Order + err = tavern.Order(uid, order) + if err != nil { + t.Error(err) + } + +}