Skip to content

Commit

Permalink
Adding "scope" (#9)
Browse files Browse the repository at this point in the history
The `scope` defines when a service should be created, or when it can be reused. It must be one of the following values:

- `prototype`: A new instance will be created whenever the service is requested or injected into another service as a dependency.

- `container` (default): The instance will created once for this container, and then it will be returned in future requests. This is sometimes called a singleton, however the service will not be shared outside of the container.

Fixes #2
  • Loading branch information
elliotchance committed Jun 20, 2019
1 parent 2586155 commit 4fdd3d9
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 90 deletions.
148 changes: 97 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Easy, fast and type-safe dependency injection for Go.
* [Installation](#installation)
* [Building the Container](#building-the-container)
* [Configuring Services](#configuring-services)
+ [error](#error)
+ [import](#import)
+ [interface](#interface)
+ [properties](#properties)
+ [returns](#returns)
+ [scope](#scope)
+ [type](#type)
* [Using Services](#using-services)
* [Unit Testing](#unit-testing)

Expand All @@ -31,75 +38,114 @@ Here is an example of a `dingo.yml`:
```yml
services:
SendEmail:
type: *SendEmail
type: '*SendEmail'
interface: EmailSender
properties:
From: '"hi@welcome.com"'

CustomerWelcome:
type: *CustomerWelcome
returns: NewCustomerWelcome(@SendEmail)
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:
The root level `services` key describes each of the services.

```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 name of the service follows the same naming conventions as Go, so service
names that start with a capital letter will be exported (available outside this
package).

All options described below are optional. However, you must provide either
`type` or `interface`.

The `returns` and properties can contain any Go code, and allows the following
substitutions:
Any option below that expects an expression can contain any valid Go code.
References to other services and variables will be substituted automatically:

- `@{SendEmail}` will inject the service named `SendEmail`.
- `${DB_PASS}` will inject the environment variable `DB_PASS`.

### error

If `returns` provides two arguments (where the second one is the error) you must
include an `error`. This is the expression when `err != nil`.

Examples:

- `error: panic(err)` - panic if an error occurs.
- `error: return nil` - return a nil service if an error occurs.

### import

You can provide explicit imports if you need to reference packages in
expressions (such as `returns`) that do not exist in `type` or `interface`.

If a package listed in `import` is already imported, either directly or
indirectly, it value will be ignored.

Example:

```yml
import:
- 'github.com/aws/aws-sdk-go/aws/session'
```

### interface

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 `returns`.

Examples:

- `interface: EmailSender` - `EmailSender` in this package.
- `interface: io.Writer` - `Writer` in the `io` package.

### properties

If provided, a map of case-sensitive properties to be set on the instance. Each
of the properties is a Go expression.

Example:

```yml
properties:
From: "hi@welcome.com"
maxRetries: 10
emailer: '@{Emailer}'
```

### returns

The expression used to instantiate the service. You can provide any Go code
here, including referencing other services and environment variables.

### scope

The `scope` defines when a service should be created, or when it can be reused.
It must be one of the following values:

- `prototype`: A new instance will be created whenever the service is requested
or injected into another service as a dependency.

- `container` (default): The instance will created once for this container, and
then it will be returned in future requests. This is sometimes called a
singleton, however the service will not be shared outside of the container.

### type

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.

Example

```:
type: '*github.com/go-redis/redis.Options'
```

## Using Services

As part of the generated file, `dingo.go`. There will be a module-level variable
Expand Down
5 changes: 5 additions & 0 deletions dingotest/dingo.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dingotest
import (
go_sub_pkg "github.com/elliotchance/dingo/dingotest/go-sub-pkg"
"os"
time "time"
)

type Container struct {
Expand All @@ -26,6 +27,10 @@ func (container *Container) GetCustomerWelcome() *CustomerWelcome {
}
return container.CustomerWelcome
}
func (container *Container) GetNow() time.Time {
service := time.Now()
return service
}
func (container *Container) GetOtherPkg() *go_sub_pkg.Person {
if container.OtherPkg == nil {
service := &go_sub_pkg.Person{}
Expand Down
5 changes: 5 additions & 0 deletions dingotest/dingo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ services:
type: string
returns: ${ShouldBeSet}

Now:
type: time.Time
returns: time.Now()
scope: prototype

OtherPkg:
type: '*github.com/elliotchance/dingo/dingotest/go-sub-pkg.Person'

Expand Down
8 changes: 8 additions & 0 deletions dingotest/dingo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ func TestContainer_GetSomeEnv(t *testing.T) {
service := container.GetSomeEnv()
assert.Equal(t, "qux", service)
}

func TestContainer_Now(t *testing.T) {
container := &dingotest.Container{}

service1 := container.GetNow()
service2 := container.GetNow()
assert.NotEqual(t, service1, service2)
}
94 changes: 59 additions & 35 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,21 @@ func main() {
for _, serviceName := range serviceNames {
definition := all.Services[serviceName]

containerFields = append(containerFields, &ast.Field{
Names: []*ast.Ident{
{Name: serviceName},
},
Type: &ast.Ident{Name: definition.InterfaceOrLocalEntityPointerType()},
})
switch definition.Scope {
case ScopeNotSet, ScopeContainer:
containerFields = append(containerFields, &ast.Field{
Names: []*ast.Ident{
{Name: serviceName},
},
Type: &ast.Ident{
Name: definition.InterfaceOrLocalEntityPointerType(),
},
})

case ScopePrototype:
// Do not create a property for this because it has to be created
// every time.
}
}

file.Decls = append(file.Decls, &ast.GenDecl{
Expand Down Expand Up @@ -200,39 +209,52 @@ func main() {
})
}

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,
},
})
// Scope
switch definition.Scope {
case ScopeNotSet, ScopeContainer:
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}},
})
}

// Return
if definition.Type.IsPointer() || definition.Interface != "" {
stmts = append(stmts, &ast.ReturnStmt{
Results: []ast.Expr{
&ast.Ident{Name: serviceVariable},
stmts = append(stmts, &ast.IfStmt{
Cond: &ast.Ident{Name: serviceVariable + " == nil"},
Body: &ast.BlockStmt{
List: instantiation,
},
})
} else {

// Returns
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},
},
})
}

case ScopePrototype:
stmts = append(stmts, instantiation...)

// Returns
stmts = append(stmts, &ast.ReturnStmt{
Results: []ast.Expr{
&ast.Ident{Name: "*" + serviceVariable},
&ast.Ident{Name: "service"},
},
})
}
Expand All @@ -253,7 +275,9 @@ func main() {
Results: &ast.FieldList{
List: []*ast.Field{
{
Type: &ast.Ident{Name: definition.InterfaceOrLocalEntityType()},
Type: &ast.Ident{
Name: definition.InterfaceOrLocalEntityType(),
},
},
},
},
Expand Down
35 changes: 31 additions & 4 deletions service.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package main

import "sort"
import (
"fmt"
"sort"
)

const (
ScopeNotSet = ""
ScopePrototype = "prototype"
ScopeContainer = "container"
)

type Service struct {
Type Type
Error string
Import []string
Interface Type
Properties map[string]string
Returns string
Error string
Import []string
Scope string
Type Type
}

func (service *Service) InterfaceOrLocalEntityType() string {
Expand Down Expand Up @@ -62,3 +72,20 @@ func (service *Service) SortedProperties() (sortedProperties []*Property) {

return
}

func (service *Service) ValidateScope() error {
switch service.Scope {
case ScopeNotSet, ScopePrototype, ScopeContainer:
return nil
}

return fmt.Errorf("invalid scope: %s", service.Scope)
}

func (service *Service) Validate() error {
if err := service.ValidateScope(); err != nil {
return err
}

return nil
}

0 comments on commit 4fdd3d9

Please sign in to comment.