Skip to content

Commit

Permalink
Support multiple ResultTags annotations
Browse files Browse the repository at this point in the history
Closes uber-go#1210

This commit makes it possible to specify multiple `fx.ResultTags` annotations in a single `fx.Annotate` call.

```go
fx.Provide(
    fx.Annotate(
        func() *bytes.Buffer {
            return bytes.NewBuffer([]byte("Hello!"))
        },
        fx.ResultTags(`name:"a"`),
        fx.ResultTags(`name:"b"`),
        fx.ResultTags(`name:"c"`),
    ),
)
```
  • Loading branch information
miyamo2 committed Jul 10, 2024
1 parent 8fa92e9 commit 2218081
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 27 deletions.
113 changes: 91 additions & 22 deletions annotated.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,15 +377,12 @@ var _ Annotation = resultTagsAnnotation{}
// If the tag is invalid and has mismatched quotation for example,
// (`tag_name:"tag_value') , this will return an error.
func (rt resultTagsAnnotation) apply(ann *annotated) error {
if len(ann.ResultTags) > 0 {
return errors.New("cannot apply more than one line of ResultTags")
}
for _, tag := range rt.tags {
if err := verifyAnnotateTag(tag); err != nil {
return err
}
}
ann.ResultTags = rt.tags
ann.ResultTags = append(ann.ResultTags, rt.tags)
return nil
}

Expand Down Expand Up @@ -426,12 +423,16 @@ func (rt resultTagsAnnotation) results(ann *annotated) (
// if there's no Out struct among the return types, there was no As annotation applied
// just replace original result types with an Out struct and apply tags
var (
newOut outStructInfo
existingOuts []reflect.Type
newOut outStructInfo
existingOuts []reflect.Type
existingOutsMapping = make(map[int][]reflect.Type)
)

newOut.Fields = []reflect.StructField{_outAnnotationField}
newOut.Offsets = []int{}
// to prevent duplicate applying of the same tag to the same type, it is kept in the following format
// {"foo": {"fmt.Stringer": struct{}{}, "myStringer": struct{}{}}, "bar": {"fmt.Stringer": struct{}{}}}
retaggedTypeMap := map[string]map[string]struct{}{}

for i, t := range types {
if !isOut(t) {
Expand All @@ -448,26 +449,92 @@ func (rt resultTagsAnnotation) results(ann *annotated) (
newOut.Fields = append(newOut.Fields, field)
continue
}
// this must be from an As annotation
// apply the tags to the existing type
// this must be from an As annotation or a ResultTags annotation
// apply the tags to the existing type if it comes from an As annotation
taggedFields := make([]reflect.StructField, t.NumField())
taggedFields[0] = _outAnnotationField
// apply the tags to the existing type after replication the existing type if it comes from a ResultTags annotation
originalTaggedFields := make([]reflect.StructField, t.NumField())
originalTaggedFields[0] = _outAnnotationField

var newlyTagged bool
var hasBeenTagged bool
for j, tag := range rt.tags {
if j+1 < t.NumField() {
field := t.Field(j + 1)
// if the field has already been tagged by ResultTags annotation, avoid overwriting it
_, taggedName := field.Tag.Lookup("name")
_, taggedGroup := field.Tag.Lookup("group")
if taggedName || taggedGroup {
originalTaggedFields[j+1] = reflect.StructField{
Name: field.Name,
Type: field.Type,
Tag: field.Tag,
}
hasBeenTagged = true
}

structTag := reflect.StructTag(tag)
if typeNames, ok := retaggedTypeMap[structTag.Get("name")]; ok {
if _, ok := typeNames[field.Type.String()]; ok {
continue
}
}
if typeNames, ok := retaggedTypeMap[structTag.Get("group")]; ok {
if _, ok := typeNames[field.Type.String()]; ok {
continue
}
}
if n, ok := structTag.Lookup("name"); ok {
typeNames, ok := retaggedTypeMap[n]
if !ok {
typeNames = make(map[string]struct{})
retaggedTypeMap[n] = typeNames
}
typeNames[field.Type.String()] = struct{}{}
}
if g, ok := structTag.Lookup("group"); ok {
typeNames, ok := retaggedTypeMap[g]
if !ok {
typeNames = make(map[string]struct{})
retaggedTypeMap[g] = typeNames
}
typeNames[field.Type.String()] = struct{}{}
}

if hasBeenTagged && !taggedName && !taggedGroup {
// if other fields are already tagged and this field is untagged, apply the new tag
originalTaggedFields[j+1] = reflect.StructField{
Name: field.Name,
Type: field.Type,
Tag: structTag,
}
continue
}
taggedFields[j+1] = reflect.StructField{
Name: field.Name,
Type: field.Type,
Tag: reflect.StructTag(tag),
Tag: structTag,
}
newlyTagged = true
}
}
existingOuts = append(existingOuts, reflect.StructOf(taggedFields))
currentTypeExistingOuts := make([]reflect.Type, 0, 2)
if hasBeenTagged {
currentTypeExistingOuts = append(currentTypeExistingOuts, reflect.StructOf(originalTaggedFields))
}
if newlyTagged {
currentTypeExistingOuts = append(currentTypeExistingOuts, reflect.StructOf(taggedFields))
}
existingOutsMapping[i] = currentTypeExistingOuts
existingOuts = append(existingOuts, currentTypeExistingOuts...)
}

resType := reflect.StructOf(newOut.Fields)

outTypes := []reflect.Type{resType}
var outTypes []reflect.Type
if len(newOut.Fields) > 1 {
outTypes = append(outTypes, resType)
}
// append existing outs back to outTypes
outTypes = append(outTypes, existingOuts...)
if hasError {
Expand All @@ -479,9 +546,10 @@ func (rt resultTagsAnnotation) results(ann *annotated) (
outErr error
outResults []reflect.Value
)
outResults = append(outResults, reflect.New(resType).Elem())

tIdx := 0
if len(newOut.Fields) > 1 {
outResults = append(outResults, reflect.New(resType).Elem())
}
existingOutResults := make([]reflect.Value, 0, len(existingOuts))
for i, r := range results {
if i == len(results)-1 && hasError {
// If hasError and this is the last item,
Expand All @@ -501,21 +569,22 @@ func (rt resultTagsAnnotation) results(ann *annotated) (
// to prevent panic from setting fx.Out to
// a value.
outResults[0].Field(fieldIdx).Set(r)
continue
}
continue
}
if isOut(r.Type()) {
tIdx++
if tIdx < len(outTypes) {
newResult := reflect.New(outTypes[tIdx]).Elem()
for j := 1; j < outTypes[tIdx].NumField(); j++ {
for _, existingOuts := range existingOutsMapping[i] {
newResult := reflect.New(existingOuts).Elem()
for j := 1; j < existingOuts.NumField(); j++ {
newResult.Field(j).Set(r.Field(j))
}
outResults = append(outResults, newResult)
existingOutResults = append(existingOutResults, newResult)
}
}
}

outResults = append(outResults, existingOutResults...)

if hasError {
if outErr != nil {
outResults = append(outResults, reflect.ValueOf(outErr))
Expand Down Expand Up @@ -1528,7 +1597,7 @@ type annotated struct {
Target interface{}
Annotations []Annotation
ParamTags []string
ResultTags []string
ResultTags [][]string
As [][]asType
From []reflect.Type
FuncPtr uintptr
Expand Down
58 changes: 54 additions & 4 deletions annotated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1431,7 +1431,7 @@ func TestAnnotate(t *testing.T) {
t.Run("specify two ResultTags", func(t *testing.T) {
t.Parallel()

app := NewForTest(t,
app := fxtest.New(t,
fx.Provide(
// This should just leave newA as it is.
fx.Annotate(
Expand All @@ -1440,12 +1440,62 @@ func TestAnnotate(t *testing.T) {
fx.ResultTags(`name:"AA"`),
),
),
fx.Invoke(newB),
fx.Invoke(
fx.Annotate(func(a, aa *a) (*b, *b) {
return newB(a), newB(aa)
}, fx.ParamTags(`name:"A"`, `name:"AA"`))),
)

err := app.Err()
require.Error(t, err)
assert.Contains(t, err.Error(), "encountered error while applying annotation using fx.Annotate to go.uber.org/fx_test.TestAnnotate.func1(): cannot apply more than one line of ResultTags")
require.NoError(t, err)
defer app.RequireStart().RequireStop()
})

t.Run("specify two ResultTags containing multiple tags", func(t *testing.T) {
t.Parallel()

app := fxtest.New(t,
fx.Provide(
fx.Annotate(
func() (*a, *b) {
return newA(), newB(&a{})
},
fx.ResultTags(`name:"A"`, `name:"B"`),
fx.ResultTags(`name:"AA"`, `name:"BB"`),
),
),
fx.Invoke(
fx.Annotate(func(a, aa *a, b, bb *b) (*b, *b, *c, *c) {
return newB(a), newB(aa), newC(b), newC(b)
}, fx.ParamTags(`name:"A"`, `name:"AA"`, `name:"B"`, `name:"BB"`))),
)

err := app.Err()
require.NoError(t, err)
defer app.RequireStart().RequireStop()
})

t.Run("specify Three ResultTags", func(t *testing.T) {
t.Parallel()

app := fxtest.New(t,
fx.Provide(
fx.Annotate(
newA,
fx.ResultTags(`name:"A"`),
fx.ResultTags(`name:"AA"`),
fx.ResultTags(`name:"AAA"`),
),
),
fx.Invoke(
fx.Annotate(func(a, aa, aaa *a) (*b, *b, *b) {
return newB(a), newB(aa), newB(aaa)
}, fx.ParamTags(`name:"A"`, `name:"AA"`, `name:"AAA"`))),
)

err := app.Err()
require.NoError(t, err)
defer app.RequireStart().RequireStop()
})

t.Run("annotate with a non-nil error", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func TestNewApp(t *testing.T) {
// cannot provide fx_test.t1[name="foo"] from [0].Field0:
// already provided by "reflect".makeFuncStub (/.../reflect/asm_amd64.s:30)
assert.Contains(t, err.Error(), `fx.Provide(fx.Annotate(`)
assert.Contains(t, err.Error(), `fx.ResultTags(["name:\"foo\""])`)
assert.Contains(t, err.Error(), `fx.ResultTags([["name:\"foo\""]])`)
assert.Contains(t, err.Error(), "already provided")
})

Expand Down

0 comments on commit 2218081

Please sign in to comment.