From 479a901f975f88820044c0cf20cf0b71f31a0b0e Mon Sep 17 00:00:00 2001 From: Elliot Chance Date: Thu, 30 May 2019 11:43:58 +1000 Subject: [PATCH] Initial commit This is the first release and contains enough to get started. --- .gitignore | 14 ++ LICENSE | 21 +++ README.md | 143 +++++++++++++++ dingotest/customer_welcome.go | 20 ++ dingotest/dingo.go | 90 +++++++++ dingotest/dingo.yml | 40 ++++ dingotest/dingo_test.go | 102 +++++++++++ dingotest/email_sender.go | 5 + dingotest/go-sub-pkg/greeter.go | 5 + dingotest/go-sub-pkg/person.go | 10 + dingotest/send_email.go | 13 ++ go.mod | 9 + go.sum | 18 ++ main.go | 314 ++++++++++++++++++++++++++++++++ service.go | 44 +++++ type.go | 80 ++++++++ type_test.go | 165 +++++++++++++++++ 17 files changed, 1093 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dingotest/customer_welcome.go create mode 100644 dingotest/dingo.go create mode 100644 dingotest/dingo.yml create mode 100644 dingotest/dingo_test.go create mode 100644 dingotest/email_sender.go create mode 100644 dingotest/go-sub-pkg/greeter.go create mode 100644 dingotest/go-sub-pkg/person.go create mode 100644 dingotest/send_email.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 service.go create mode 100644 type.go create mode 100644 type_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea7021b --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +/.idea +/dingo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cf60a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Elliot Chance + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..870ed44 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# 🐺 dingo + +Easy, fast and type-safe dependency injection for Go. + + * [Installation](#installation) + * [Building the Container](#building-the-container) + * [Configuring Services](#configuring-services) + * [Using Services](#using-services) + * [Unit Testing](#unit-testing) + +## Installation + +```bash +go get -u github.com/elliotchance/dingo +``` + +## Building the Container + +Building or rebuilding the container is done with: + +```bash +dingo +``` + +The container is created from a file called `dingo.yml` in the same directory as +where the `dingo` command is run. This should be the root of your +module/repository. + +Here is an example of a `dingo.yml`: + +```yml +services: + SendEmail: + type: *SendEmail + interface: EmailSender + properties: + From: '"hi@welcome.com"' + + CustomerWelcome: + type: *CustomerWelcome + returns: NewCustomerWelcome(@SendEmail) +``` + +It will generate a file called `dingo.go`. This must be committed with your +code. + +## Configuring Services + +The `dingo.yml` is described below: + +```yml +services: + # Describes each of the services. The name of the service follow the same + # naming conventions as Go, so service names that start with a capital letter + # will be exported (available outside this package). + SendEmail: + + # Required: You must provide either 'type' or 'interface'. + + # Optional: The type returned by the `return` expression. You must provide a + # fully qualified name that includes the package name if the type does not + # belong to this package. For example: + # + # type: '*github.com/go-redis/redis.Options' + # + type: *SendEmail + + # Optional: If you need to replace this service with another struct type in + # unit tests you will need to provide an interface. This will override + # `type` and must be compatible with returned type of `return`. + interface: EmailSender + + # Optional: The expression used to instantiate the service. You can provide + # any Go code here, including referencing other services and environment + # variables. Described in more detail below. + returns: NewSendEmail() + + # Optional: If 'returns' provides two arguments (where the second one is the + # error) you must include an 'error'. This is the expression when + # "err != nil". + error: panic(err) + + # Optional: If provided, a map of case-sensitive properties to be set on the + # instance. Each of the properties is Go code and can have the same + # substitutions described below. + properties: + From: "hi@welcome.com" + maxRetries: 10 + + # Optional: You can provide explicit imports if you need to reference + # packages in expressions (such as 'returns') that do not exist 'type' or + # 'interface'. + import: + - 'github.com/aws/aws-sdk-go/aws/session' +``` + +The `returns` and properties can contain any Go code, and allows the following +substitutions: + +- `@{SendEmail}` will inject the service named `SendEmail`. +- `${DB_PASS}` will inject the environment variable `DB_PASS`. + +## Using Services + +As part of the generated file, `dingo.go`. There will be a module-level variable +called `DefaultContainer`. This requires no initialization and can be used +immediately: + +```go +func main() { + welcomer := DefaultContainer.GetCustomerWelcome() + err := welcomer.Welcome("Bob", "bob@smith.com") + // ... +} +``` + +## Unit Testing + +**When unit testing you should not use the global `DefaultContainer`.** You +should create a new container: + +```go +container := &Container{} +``` + +Unit tests can make any modifications to the new container, including overriding +services to provide mocks or other stubs: + +```go +func TestCustomerWelcome_Welcome(t *testing.T) { + emailer := FakeEmailSender{} + emailer.On("Send", + "bob@smith.com", "Welcome", "Hi, Bob!").Return(nil) + + container := &Container{} + container.SendEmail = emailer + + welcomer := container.GetCustomerWelcome() + err := welcomer.Welcome("Bob", "bob@smith.com") + assert.NoError(t, err) + emailer.AssertExpectations(t) +} +``` diff --git a/dingotest/customer_welcome.go b/dingotest/customer_welcome.go new file mode 100644 index 0000000..b06179e --- /dev/null +++ b/dingotest/customer_welcome.go @@ -0,0 +1,20 @@ +package dingotest + +import "fmt" + +type CustomerWelcome struct { + Emailer EmailSender +} + +func NewCustomerWelcome(sender EmailSender) *CustomerWelcome { + return &CustomerWelcome{ + Emailer: sender, + } +} + +func (welcomer *CustomerWelcome) Welcome(name, email string) error { + body := fmt.Sprintf("Hi, %s!", name) + subject := "Welcome" + + return welcomer.Emailer.Send(email, subject, body) +} diff --git a/dingotest/dingo.go b/dingotest/dingo.go new file mode 100644 index 0000000..48fedfd --- /dev/null +++ b/dingotest/dingo.go @@ -0,0 +1,90 @@ +package dingotest + +import ( + go_sub_pkg "github.com/elliotchance/dingo/dingotest/go-sub-pkg" + "os" +) + +type Container struct { + CustomerWelcome *CustomerWelcome + OtherPkg *go_sub_pkg.Person + OtherPkg2 go_sub_pkg.Greeter + OtherPkg3 *go_sub_pkg.Person + SendEmail EmailSender + SendEmailError *SendEmail + SomeEnv *string + WithEnv1 *SendEmail + WithEnv2 *SendEmail +} + +var DefaultContainer = &Container{} + +func (container *Container) GetCustomerWelcome() *CustomerWelcome { + if container.CustomerWelcome == nil { + service := NewCustomerWelcome(container.GetSendEmail()) + container.CustomerWelcome = service + } + return container.CustomerWelcome +} +func (container *Container) GetOtherPkg() *go_sub_pkg.Person { + if container.OtherPkg == nil { + service := &go_sub_pkg.Person{} + container.OtherPkg = service + } + return container.OtherPkg +} +func (container *Container) GetOtherPkg2() go_sub_pkg.Greeter { + if container.OtherPkg2 == nil { + service := go_sub_pkg.NewPerson() + container.OtherPkg2 = service + } + return container.OtherPkg2 +} +func (container *Container) GetOtherPkg3() go_sub_pkg.Person { + if container.OtherPkg3 == nil { + service := go_sub_pkg.Person{} + container.OtherPkg3 = &service + } + return *container.OtherPkg3 +} +func (container *Container) GetSendEmail() EmailSender { + if container.SendEmail == nil { + service := &SendEmail{} + service.From = "hi@welcome.com" + container.SendEmail = service + } + return container.SendEmail +} +func (container *Container) GetSendEmailError() *SendEmail { + if container.SendEmailError == nil { + service, err := NewSendEmail() + if err != nil { + panic(err) + } + container.SendEmailError = service + } + return container.SendEmailError +} +func (container *Container) GetSomeEnv() string { + if container.SomeEnv == nil { + service := os.Getenv("ShouldBeSet") + container.SomeEnv = &service + } + return *container.SomeEnv +} +func (container *Container) GetWithEnv1() SendEmail { + if container.WithEnv1 == nil { + service := SendEmail{} + service.From = os.Getenv("ShouldBeSet") + container.WithEnv1 = &service + } + return *container.WithEnv1 +} +func (container *Container) GetWithEnv2() *SendEmail { + if container.WithEnv2 == nil { + service := &SendEmail{} + service.From = "foo-" + os.Getenv("ShouldBeSet") + "-bar" + container.WithEnv2 = service + } + return container.WithEnv2 +} diff --git a/dingotest/dingo.yml b/dingotest/dingo.yml new file mode 100644 index 0000000..91090a5 --- /dev/null +++ b/dingotest/dingo.yml @@ -0,0 +1,40 @@ +services: + SendEmail: + type: '*SendEmail' + interface: EmailSender + properties: + From: '"hi@welcome.com"' + + CustomerWelcome: + type: '*CustomerWelcome' + returns: NewCustomerWelcome(@{SendEmail}) + + WithEnv1: + type: SendEmail + properties: + From: ${ShouldBeSet} + + WithEnv2: + type: '*SendEmail' + properties: + From: '"foo-" + ${ShouldBeSet} + "-bar"' + + SomeEnv: + type: string + returns: ${ShouldBeSet} + + OtherPkg: + type: '*github.com/elliotchance/dingo/dingotest/go-sub-pkg.Person' + + OtherPkg2: + type: '*github.com/elliotchance/dingo/dingotest/go-sub-pkg.Person' + interface: github.com/elliotchance/dingo/dingotest/go-sub-pkg.Greeter + returns: go_sub_pkg.NewPerson() + + OtherPkg3: + type: github.com/elliotchance/dingo/dingotest/go-sub-pkg.Person + + SendEmailError: + type: '*SendEmail' + returns: NewSendEmail() + error: panic(err) diff --git a/dingotest/dingo_test.go b/dingotest/dingo_test.go new file mode 100644 index 0000000..4dccbbe --- /dev/null +++ b/dingotest/dingo_test.go @@ -0,0 +1,102 @@ +package dingotest_test + +import ( + "github.com/elliotchance/dingo/dingotest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "os" + "testing" +) + +type FakeEmailSender struct { + mock.Mock +} + +func (mock *FakeEmailSender) Send(to, subject, body string) error { + args := mock.Called(to, subject, body) + return args.Error(0) +} + +func TestMain(m *testing.M) { + _ = os.Setenv("ShouldBeSet", "qux") + os.Exit(m.Run()) +} + +func TestCustomerWelcome_Welcome(t *testing.T) { + emailer := &FakeEmailSender{} + emailer.On("Send", + "bob@smith.com", "Welcome", "Hi, Bob!").Return(nil) + + container := &dingotest.Container{} + container.SendEmail = emailer + + welcomer := container.GetCustomerWelcome() + err := welcomer.Welcome("Bob", "bob@smith.com") + assert.NoError(t, err) + emailer.AssertExpectations(t) +} + +func TestDefaultContainer(t *testing.T) { + assert.NotNil(t, dingotest.DefaultContainer) + assert.Nil(t, dingotest.DefaultContainer.SendEmail) + assert.Nil(t, dingotest.DefaultContainer.CustomerWelcome) +} + +func TestContainer_GetSendEmail(t *testing.T) { + container := &dingotest.Container{} + + assert.Nil(t, container.SendEmail) + + // Is singleton. + service1 := container.GetSendEmail() + assert.IsType(t, (*dingotest.SendEmail)(nil), service1) + + service2 := container.GetSendEmail() + assert.IsType(t, (*dingotest.SendEmail)(nil), service2) + assert.Exactly(t, service1, service2) + + // Properties + assert.Equal(t, "hi@welcome.com", service1.(*dingotest.SendEmail).From) + assert.Equal(t, "hi@welcome.com", service2.(*dingotest.SendEmail).From) + + assert.NotNil(t, container.SendEmail) +} + +func TestContainer_GetCustomerWelcome(t *testing.T) { + container := &dingotest.Container{} + + assert.Nil(t, container.SendEmail) + assert.Nil(t, container.CustomerWelcome) + + // Is singleton. + service1 := container.GetCustomerWelcome() + assert.IsType(t, (*dingotest.CustomerWelcome)(nil), service1) + + service2 := container.GetCustomerWelcome() + assert.IsType(t, (*dingotest.CustomerWelcome)(nil), service2) + assert.Exactly(t, service1, service2) + + assert.NotNil(t, container.SendEmail) + assert.NotNil(t, container.CustomerWelcome) +} + +func TestContainer_GetWithEnv1(t *testing.T) { + container := &dingotest.Container{} + + service := container.GetWithEnv1() + assert.Equal(t, "qux", service.From) +} + +func TestContainer_GetWithEnv2(t *testing.T) { + container := &dingotest.Container{} + + service := container.GetWithEnv2() + assert.Equal(t, "foo-qux-bar", service.From) +} + +func TestContainer_GetSomeEnv(t *testing.T) { + container := &dingotest.Container{} + + service := container.GetSomeEnv() + assert.Equal(t, "qux", service) +} diff --git a/dingotest/email_sender.go b/dingotest/email_sender.go new file mode 100644 index 0000000..fe749c7 --- /dev/null +++ b/dingotest/email_sender.go @@ -0,0 +1,5 @@ +package dingotest + +type EmailSender interface { + Send(to, subject, body string) error +} diff --git a/dingotest/go-sub-pkg/greeter.go b/dingotest/go-sub-pkg/greeter.go new file mode 100644 index 0000000..c40f454 --- /dev/null +++ b/dingotest/go-sub-pkg/greeter.go @@ -0,0 +1,5 @@ +package go_sub_pkg + +type Greeter interface { + SayHello() +} diff --git a/dingotest/go-sub-pkg/person.go b/dingotest/go-sub-pkg/person.go new file mode 100644 index 0000000..1b41163 --- /dev/null +++ b/dingotest/go-sub-pkg/person.go @@ -0,0 +1,10 @@ +package go_sub_pkg + +type Person struct{} + +func NewPerson() *Person { + return nil +} + +func (p *Person) SayHello() { +} diff --git a/dingotest/send_email.go b/dingotest/send_email.go new file mode 100644 index 0000000..b540796 --- /dev/null +++ b/dingotest/send_email.go @@ -0,0 +1,13 @@ +package dingotest + +type SendEmail struct { + From string +} + +func (sender *SendEmail) Send(to, subject, body string) error { + return nil +} + +func NewSendEmail() (*SendEmail, error) { + return &SendEmail{}, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..11785a9 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/elliotchance/dingo + +go 1.12 + +require ( + github.com/stretchr/testify v1.3.0 + golang.org/x/tools v0.0.0-20190530001615-b97706b7f64d + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..782b7b2 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190530001615-b97706b7f64d h1:uVrwmEsn22e4befeQ6fUruML6nom3W7Z/KjHzNEJmAw= +golang.org/x/tools v0.0.0-20190530001615-b97706b7f64d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..bac37b4 --- /dev/null +++ b/main.go @@ -0,0 +1,314 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "golang.org/x/tools/go/ast/astutil" + "gopkg.in/yaml.v2" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +var fset *token.FileSet +var file *ast.File + +type File struct { + Services map[string]Service +} + +func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { + result := "" + lastIndex := 0 + + for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { + var groups []string + for i := 0; i < len(v); i += 2 { + groups = append(groups, str[v[i]:v[i+1]]) + } + + result += str[lastIndex:v[0]] + repl(groups) + lastIndex = v[1] + } + + return result + str[lastIndex:] +} + +func resolveStatement(stmt string) string { + // Replace environment variables. + stmt = replaceAllStringSubmatchFunc( + regexp.MustCompile(`\${(.*?)}`), stmt, func(i []string) string { + astutil.AddImport(fset, file, "os") + + return fmt.Sprintf("os.Getenv(\"%s\")", i[1]) + }) + + // Replace service names. + stmt = replaceAllStringSubmatchFunc( + regexp.MustCompile(`@{(.*?)}`), stmt, func(i []string) string { + return fmt.Sprintf("container.Get%s()", i[1]) + }) + + return stmt +} + +func main() { + dingoYMLPath := "dingo.yml" + outputFile := "dingo.go" + + f, err := ioutil.ReadFile(dingoYMLPath) + if err != nil { + log.Fatalln("reading file:", err) + } + + all := File{} + err = yaml.Unmarshal(f, &all) + if err != nil { + log.Fatalln("yaml:", err) + } + + fset = token.NewFileSet() + packageLine := fmt.Sprintf("package %s", getPackageName(dingoYMLPath)) + file, err = parser.ParseFile(fset, outputFile, packageLine, 0) + if err != nil { + log.Fatalln("parser:", err) + } + + // Sort services to the output file is neat and deterministic. + var serviceNames []string + for name := range all.Services { + serviceNames = append(serviceNames, name) + } + + sort.Strings(serviceNames) + + // type Container struct + var containerFields []*ast.Field + for _, serviceName := range serviceNames { + definition := all.Services[serviceName] + + containerFields = append(containerFields, &ast.Field{ + Names: []*ast.Ident{ + {Name: serviceName}, + }, + Type: &ast.Ident{Name: definition.InterfaceOrLocalEntityPointerType()}, + }) + } + + file.Decls = append(file.Decls, &ast.GenDecl{ + Tok: token.TYPE, + Specs: []ast.Spec{ + &ast.TypeSpec{ + Name: &ast.Ident{Name: "Container"}, + Type: &ast.StructType{ + Fields: &ast.FieldList{ + List: containerFields, + }, + }, + }, + }, + }) + + file.Decls = append(file.Decls, &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{ + &ast.ValueSpec{ + Names: []*ast.Ident{ + {Name: "DefaultContainer"}, + }, + Values: []ast.Expr{ + &ast.Ident{Name: "&Container{}"}, + }, + }, + }, + }) + + for _, serviceName := range serviceNames { + definition := all.Services[serviceName] + + // Add imports for type, interface and explicit imports. + for packageName, shortName := range definition.Imports() { + astutil.AddNamedImport(fset, file, shortName, packageName) + } + + returnTypeParts := strings.Split( + regexp.MustCompile(`/v\d+\.`).ReplaceAllString(string(definition.Type), "."), "/") + returnType := returnTypeParts[len(returnTypeParts)-1] + if strings.HasPrefix(string(definition.Type), "*") && !strings.HasPrefix(returnType, "*") { + returnType = "*" + returnType + } + + var stmts, instantiation []ast.Stmt + serviceVariable := "container." + serviceName + serviceTempVariable := "service" + + // Instantiation + if definition.Returns == "" { + instantiation = []ast.Stmt{ + &ast.AssignStmt{ + Tok: token.DEFINE, + Lhs: []ast.Expr{&ast.Ident{Name: serviceTempVariable}}, + Rhs: []ast.Expr{ + &ast.CompositeLit{ + Type: &ast.Ident{Name: definition.Type.CreateLocalEntityType()}, + }, + }, + }, + } + } else { + lhs := []ast.Expr{&ast.Ident{Name: serviceTempVariable}} + + if definition.Error != "" { + lhs = append(lhs, &ast.Ident{Name: "err"}) + } + + instantiation = []ast.Stmt{ + &ast.AssignStmt{ + Tok: token.DEFINE, + Lhs: lhs, + Rhs: []ast.Expr{&ast.Ident{Name: resolveStatement(definition.Returns)}}, + }, + } + + if definition.Error != "" { + instantiation = append(instantiation, &ast.IfStmt{ + Cond: &ast.Ident{Name: "err != nil"}, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ExprStmt{ + X: &ast.Ident{Name: definition.Error}, + }, + }, + }, + }) + } + } + + // Properties + for propertyName, propertyValue := range definition.Properties { + instantiation = append(instantiation, &ast.AssignStmt{ + Tok: token.ASSIGN, + Lhs: []ast.Expr{&ast.Ident{Name: serviceTempVariable + "." + propertyName}}, + Rhs: []ast.Expr{&ast.Ident{Name: resolveStatement(propertyValue)}}, + }) + } + + if definition.Type.IsPointer() || definition.Interface != "" { + instantiation = append(instantiation, &ast.AssignStmt{ + Tok: token.ASSIGN, + Lhs: []ast.Expr{&ast.Ident{Name: serviceVariable}}, + Rhs: []ast.Expr{&ast.Ident{Name: serviceTempVariable}}, + }) + } else { + instantiation = append(instantiation, &ast.AssignStmt{ + Tok: token.ASSIGN, + Lhs: []ast.Expr{&ast.Ident{Name: serviceVariable}}, + Rhs: []ast.Expr{&ast.Ident{Name: "&" + serviceTempVariable}}, + }) + } + + // Singleton + stmts = append(stmts, &ast.IfStmt{ + Cond: &ast.Ident{Name: serviceVariable + " == nil"}, + Body: &ast.BlockStmt{ + List: instantiation, + }, + }) + + // Return + if definition.Type.IsPointer() || definition.Interface != "" { + stmts = append(stmts, &ast.ReturnStmt{ + Results: []ast.Expr{ + &ast.Ident{Name: serviceVariable}, + }, + }) + } else { + stmts = append(stmts, &ast.ReturnStmt{ + Results: []ast.Expr{ + &ast.Ident{Name: "*" + serviceVariable}, + }, + }) + } + + file.Decls = append(file.Decls, &ast.FuncDecl{ + Name: &ast.Ident{Name: "Get" + serviceName}, + Recv: &ast.FieldList{ + List: []*ast.Field{ + { + Names: []*ast.Ident{ + {Name: "container"}, + }, + Type: &ast.Ident{Name: "*Container"}, + }, + }, + }, + Type: &ast.FuncType{ + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: &ast.Ident{Name: definition.InterfaceOrLocalEntityType()}, + }, + }, + }, + }, + Body: &ast.BlockStmt{ + List: stmts, + }, + }) + } + + ast.SortImports(fset, file) + + outFile, err := os.Create(outputFile) + if err != nil { + log.Fatalln("open file:", err) + } + + err = printer.Fprint(outFile, fset, file) + if err != nil { + log.Fatalln("writer:", err) + } +} + +func getPackageName(dingoYMLPath string) string { + abs, err := filepath.Abs(dingoYMLPath) + if err != nil { + panic(err) + } + + // The directory name is not enough because it may contain a command + // (package main). Find the first non-test file to get the real package + // name. + dir := filepath.Dir(abs) + files, err := ioutil.ReadDir(dir) + if err != nil { + panic(err) + } + + for _, fileInfo := range files { + if strings.HasSuffix(fileInfo.Name(), ".go") && + !strings.HasSuffix(fileInfo.Name(), "_test.go") { + f, err := ioutil.ReadFile(dir + "/" + fileInfo.Name()) + if err != nil { + panic(err) + } + + file, err = parser.ParseFile(fset, fileInfo.Name(), f, 0) + if err != nil { + panic(err) + } + + return file.Name.String() + } + } + + // Couldn't find the package name. Assume command. + return "main" +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..f135571 --- /dev/null +++ b/service.go @@ -0,0 +1,44 @@ +package main + +type Service struct { + Type Type + Interface Type + Properties map[string]string + Returns string + Error string + Import []string +} + +func (service *Service) InterfaceOrLocalEntityType() string { + if service.Interface != "" { + return service.Interface.LocalEntityType() + } + + return service.Type.LocalEntityType() +} + +func (service *Service) InterfaceOrLocalEntityPointerType() string { + if service.Interface != "" { + return service.Interface.LocalEntityType() + } + + return service.Type.LocalEntityPointerType() +} + +func (service *Service) Imports() map[string]string { + imports := map[string]string{} + + for _, packageName := range service.Import { + imports[packageName] = "" + } + + if service.Type.PackageName() != "" { + imports[service.Type.PackageName()] = service.Type.LocalPackageName() + } + + if service.Interface.PackageName() != "" { + imports[service.Interface.PackageName()] = service.Interface.LocalPackageName() + } + + return imports +} diff --git a/type.go b/type.go new file mode 100644 index 0000000..6d28d69 --- /dev/null +++ b/type.go @@ -0,0 +1,80 @@ +package main + +import ( + "regexp" + "strings" +) + +type Type string + +func (ty Type) String() string { + return string(ty) +} + +func (ty Type) IsPointer() bool { + return strings.HasPrefix(string(ty), "*") +} + +func (ty Type) PackageName() string { + if !strings.Contains(string(ty), ".") { + return "" + } + + parts := strings.Split(strings.TrimLeft(string(ty), "*"), ".") + return strings.Join(parts[:len(parts)-1], ".") +} + +func (ty Type) UnversionedPackageName() string { + packageName := strings.Split(ty.PackageName(), "/") + if regexp.MustCompile(`^v\d+$`).MatchString(packageName[len(packageName)-1]) { + packageName = packageName[:len(packageName)-1] + } + + return strings.Join(packageName, "/") +} + +func (ty Type) LocalPackageName() string { + pkgNameParts := strings.Split(ty.UnversionedPackageName(), "/") + lastPart := pkgNameParts[len(pkgNameParts)-1] + + return strings.Replace(lastPart, "-", "_", -1) +} + +func (ty Type) EntityName() string { + parts := strings.Split(string(ty), ".") + + return strings.TrimLeft(parts[len(parts)-1], "*") +} + +func (ty Type) LocalEntityName() string { + name := ty.LocalPackageName() + "." + ty.EntityName() + + return strings.TrimLeft(name, ".") +} + +func (ty Type) LocalEntityType() string { + name := ty.LocalEntityName() + if ty.IsPointer() { + name = "*" + name + } + + return name +} + +func (ty Type) CreateLocalEntityType() string { + name := ty.LocalEntityName() + if ty.IsPointer() { + name = "&" + name + } + + return name +} + +func (ty Type) LocalEntityPointerType() string { + name := ty.LocalEntityName() + if !strings.HasPrefix(name, "*") { + name = "*" + name + } + + return name +} diff --git a/type_test.go b/type_test.go new file mode 100644 index 0000000..e16ff70 --- /dev/null +++ b/type_test.go @@ -0,0 +1,165 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +var typeTests = map[Type]struct { + IsPointer bool + PackageName string + LocalPackageName string + EntityName string + LocalEntityName string + LocalEntityType string + CreateLocalEntityType string + LocalEntityPointerType string + UnversionedPackageName string +}{ + "Person": { + IsPointer: false, + PackageName: "", + LocalPackageName: "", + EntityName: "Person", + LocalEntityName: "Person", + LocalEntityType: "Person", + CreateLocalEntityType: "Person", + LocalEntityPointerType: "*Person", + UnversionedPackageName: "", + }, + "*Person": { + IsPointer: true, + PackageName: "", + LocalPackageName: "", + EntityName: "Person", + LocalEntityName: "Person", + LocalEntityType: "*Person", + CreateLocalEntityType: "&Person", + LocalEntityPointerType: "*Person", + UnversionedPackageName: "", + }, + "github.com/elliotchance/dingo/dingotest/go-sub-pkg.Person": { + IsPointer: false, + PackageName: "github.com/elliotchance/dingo/dingotest/go-sub-pkg", + LocalPackageName: "go_sub_pkg", + EntityName: "Person", + LocalEntityName: "go_sub_pkg.Person", + LocalEntityType: "go_sub_pkg.Person", + CreateLocalEntityType: "go_sub_pkg.Person", + LocalEntityPointerType: "*go_sub_pkg.Person", + UnversionedPackageName: "github.com/elliotchance/dingo/dingotest/go-sub-pkg", + }, + "*github.com/elliotchance/dingo/dingotest/go-sub-pkg.Person": { + IsPointer: true, + PackageName: "github.com/elliotchance/dingo/dingotest/go-sub-pkg", + LocalPackageName: "go_sub_pkg", + EntityName: "Person", + LocalEntityName: "go_sub_pkg.Person", + LocalEntityType: "*go_sub_pkg.Person", + CreateLocalEntityType: "&go_sub_pkg.Person", + LocalEntityPointerType: "*go_sub_pkg.Person", + UnversionedPackageName: "github.com/elliotchance/dingo/dingotest/go-sub-pkg", + }, + "github.com/kounta/luigi/v7.Logger": { + IsPointer: false, + PackageName: "github.com/kounta/luigi/v7", + LocalPackageName: "luigi", + EntityName: "Logger", + LocalEntityName: "luigi.Logger", + LocalEntityType: "luigi.Logger", + CreateLocalEntityType: "luigi.Logger", + LocalEntityPointerType: "*luigi.Logger", + UnversionedPackageName: "github.com/kounta/luigi", + }, + "*github.com/kounta/luigi/v7.SimpleLogger": { + IsPointer: true, + PackageName: "github.com/kounta/luigi/v7", + LocalPackageName: "luigi", + EntityName: "SimpleLogger", + LocalEntityName: "luigi.SimpleLogger", + LocalEntityType: "*luigi.SimpleLogger", + CreateLocalEntityType: "&luigi.SimpleLogger", + LocalEntityPointerType: "*luigi.SimpleLogger", + UnversionedPackageName: "github.com/kounta/luigi", + }, +} + +func TestType_String(t *testing.T) { + for ty := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, string(ty), ty.String()) + }) + } +} + +func TestType_IsPointer(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.IsPointer, ty.IsPointer()) + }) + } +} + +func TestType_PackageName(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.PackageName, ty.PackageName()) + }) + } +} + +func TestType_LocalPackageName(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.LocalPackageName, ty.LocalPackageName()) + }) + } +} + +func TestType_EntityName(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.EntityName, ty.EntityName()) + }) + } +} + +func TestType_LocalEntityName(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.LocalEntityName, ty.LocalEntityName()) + }) + } +} + +func TestType_LocalEntityType(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.LocalEntityType, ty.LocalEntityType()) + }) + } +} + +func TestType_CreateLocalEntityType(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.CreateLocalEntityType, ty.CreateLocalEntityType()) + }) + } +} + +func TestType_LocalEntityPointerType(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.LocalEntityPointerType, ty.LocalEntityPointerType()) + }) + } +} + +func TestType_UnversionedPackageName(t *testing.T) { + for ty, test := range typeTests { + t.Run(string(ty), func(t *testing.T) { + assert.Equal(t, test.UnversionedPackageName, ty.UnversionedPackageName()) + }) + } +}