Skip to content

Commit

Permalink
Private Provides (#995)
Browse files Browse the repository at this point in the history
This change adds the ability to tag provides as "private", meaning the
provided constructors will be added specifically to that module's scope,
rather than the root Dig container scope, restricting usage of them to
that module and modules it contains only.

Users can easily do this by adding `fx.Private` in their call to
`fx.Provide()`:

```go
app := fx.New(
    fx.Module("SubModule",
        fx.Provide(
            MyPrivateProvideFunc,
            fx.Private // Now only accessible within SubModule
        )
    ),
    fx.Invoke(MyInvokeFunc) // Cannot rely on "MyPrivateProvideFunc"
)
```

Refs: #856

Co-authored-by: Sung Yoon Whang <sungyoon@uber.com>
  • Loading branch information
JacobOaks and sywhang authored Dec 15, 2022
1 parent 93a9a6d commit 6d76d64
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 6 deletions.
3 changes: 3 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ type provide struct {
// IsSupply is true when the Target constructor was emitted by fx.Supply.
IsSupply bool
SupplyType reflect.Type // set only if IsSupply

// Set if the type should be provided at private scope.
Private bool
}

// invoke is a single invocation request to Fx.
Expand Down
130 changes: 130 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,136 @@ func TestNewApp(t *testing.T) {
})
}

func TestPrivateProvide(t *testing.T) {
t.Parallel()

t.Run("CanUsePrivateAndNonPrivateFromOuterModule", func(t *testing.T) {
t.Parallel()

app := fxtest.New(t,
Module("SubModule", Invoke(func(a int, b string) {})),
Provide(func() int { return 0 }, Private),
Provide(func() string { return "" }),
)
app.RequireStart().RequireStop()
})

t.Run("CantUsePrivateFromSubModule", func(t *testing.T) {
t.Parallel()

app := New(
Module("SubModule", Provide(func() int { return 0 }, Private)),
Invoke(func(a int) {}),
)
err := app.Err()
require.Error(t, err)
assert.Contains(t, err.Error(), "missing dependencies for function")
assert.Contains(t, err.Error(), "missing type: int")
})

t.Run("DifferentModulesCanProvideSamePrivateType", func(t *testing.T) {
t.Parallel()

app := fxtest.New(t,
Module("SubModuleA",
Provide(func() int { return 1 }, Private),
Invoke(func(s int) {
assert.Equal(t, 1, s)
}),
),
Module("SubModuleB",
Provide(func() int { return 2 }, Private),
Invoke(func(s int) {
assert.Equal(t, 2, s)
}),
),
Provide(func() int { return 3 }),
Invoke(func(s int) {
assert.Equal(t, 3, s)
}),
)
app.RequireStart().RequireStop()
})
}

func TestPrivateProvideWithDecorators(t *testing.T) {
t.Parallel()

t.Run("DecoratedPublicOrPrivateTypeInSubModule", func(t *testing.T) {
t.Parallel()

runApp := func(private bool) {
provideOpts := []interface{}{func() int { return 0 }}
if private {
provideOpts = append(provideOpts, Private)
}
app := New(
Module("SubModule",
Provide(provideOpts...),
Decorate(func(a int) int { return a + 2 }),
Invoke(func(a int) { assert.Equal(t, 2, a) }),
),
Invoke(func(a int) { assert.Equal(t, 0, a) }),
)
err := app.Err()
if private {
require.Error(t, err)
assert.Contains(t, err.Error(), "missing dependencies for function")
assert.Contains(t, err.Error(), "missing type: int")
} else {
require.NoError(t, err)
}
}

t.Run("Public", func(t *testing.T) { runApp(false) })
t.Run("Private", func(t *testing.T) { runApp(true) })
})

t.Run("DecoratedPublicOrPrivateTypeInOuterModule", func(t *testing.T) {
t.Parallel()

runApp := func(private bool) {
provideOpts := []interface{}{func() int { return 0 }}
if private {
provideOpts = append(provideOpts, Private)
}
app := fxtest.New(t,
Provide(provideOpts...),
Decorate(func(a int) int { return a - 5 }),
Invoke(func(a int) {
assert.Equal(t, -5, a)
}),
Module("Child",
Decorate(func(a int) int { return a + 10 }),
Invoke(func(a int) {
assert.Equal(t, 5, a)
}),
),
)
app.RequireStart().RequireStop()
}

t.Run("Public", func(t *testing.T) { runApp(false) })
t.Run("Private", func(t *testing.T) { runApp(true) })
})

t.Run("CannotDecoratePrivateChildType", func(t *testing.T) {
t.Parallel()

app := New(
Module("Child",
Provide(func() int { return 0 }, Private),
),
Decorate(func(a int) int { return a + 5 }),
Invoke(func(a int) {}),
)
err := app.Err()
require.Error(t, err)
assert.Contains(t, err.Error(), "missing dependencies for function")
assert.Contains(t, err.Error(), "missing type: int")
})
}

func TestWithLoggerErrorUseDefault(t *testing.T) {
// This test cannot be run in paralllel with the others because
// it hijacks stderr.
Expand Down
11 changes: 11 additions & 0 deletions docs/ex/modules/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,20 @@ import (
// region provide
// region invoke
// region decorate
// region private
var Module = fx.Module("server",
// endregion start
fx.Provide(
New,
// endregion provide
// endregion invoke
// endregion decorate
),
fx.Provide(
fx.Private,
// region provide
// region invoke
// region decorate
parseConfig,
),
// endregion provide
Expand All @@ -48,6 +58,7 @@ var Module = fx.Module("server",
// endregion invoke
// endregion provide
// endregion decorate
// endregion private

// Config is the configuration of the server.
// region config
Expand Down
24 changes: 23 additions & 1 deletion docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ To write an Fx module:
)
```

4. Lastly, if your module needs to decorate its dependencies
4. If your module needs to decorate its dependencies
before consuming them, add an `fx.Decorate` call for it.

```go mdox-exec='region ex/modules/module.go decorate'
Expand All @@ -57,6 +57,28 @@ To write an Fx module:
)
```

5. Lastly, if you want to keep a constructor's outputs contained
to your module (and modules your module includes), you can
add an `fx.Private` when providing.

```go mdox-exec='region ex/modules/module.go private'
var Module = fx.Module("server",
fx.Provide(
New,
),
fx.Provide(
fx.Private,
parseConfig,
),
fx.Invoke(startServer),
fx.Decorate(wrapLogger),
)
```

In this case, `parseConfig` is now private to the "server" module.
No modules that contain "server" will be able to use the resulting
`Config` type because it can only be seen by the "server" module.

That's all there's to writing modules.
The rest of this section covers standards and conventions
we've established for writing Fx modules at Uber.
Expand Down
8 changes: 6 additions & 2 deletions fxevent/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,15 @@ func (l *ConsoleLogger) LogEvent(event Event) {
l.logf("SUPPLY\t%v", e.TypeName)
}
case *Provided:
var privateStr string
if e.Private {
privateStr = " (PRIVATE)"
}
for _, rtype := range e.OutputTypeNames {
if e.ModuleName != "" {
l.logf("PROVIDE\t%v <= %v from module %q", rtype, e.ConstructorName, e.ModuleName)
l.logf("PROVIDE%v\t%v <= %v from module %q", privateStr, rtype, e.ConstructorName, e.ModuleName)
} else {
l.logf("PROVIDE\t%v <= %v", rtype, e.ConstructorName)
l.logf("PROVIDE%v\t%v <= %v", privateStr, rtype, e.ConstructorName)
}
}
if e.Err != nil {
Expand Down
21 changes: 21 additions & 0 deletions fxevent/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,39 @@ func TestConsoleLogger(t *testing.T) {
give: &Provided{
ConstructorName: "bytes.NewBuffer()",
OutputTypeNames: []string{"*bytes.Buffer"},
Private: false,
},
want: "[Fx] PROVIDE *bytes.Buffer <= bytes.NewBuffer()\n",
},
{
name: "Provided privately",
give: &Provided{
ConstructorName: "bytes.NewBuffer()",
OutputTypeNames: []string{"*bytes.Buffer"},
Private: true,
},
want: "[Fx] PROVIDE (PRIVATE) *bytes.Buffer <= bytes.NewBuffer()\n",
},
{
name: "Provided with module",
give: &Provided{
ConstructorName: "bytes.NewBuffer()",
ModuleName: "myModule",
OutputTypeNames: []string{"*bytes.Buffer"},
Private: false,
},
want: "[Fx] PROVIDE *bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
},
{
name: "Provided with module privately",
give: &Provided{
ConstructorName: "bytes.NewBuffer()",
ModuleName: "myModule",
OutputTypeNames: []string{"*bytes.Buffer"},
Private: true,
},
want: "[Fx] PROVIDE (PRIVATE) *bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
},
{
name: "Replaced",
give: &Replaced{
Expand Down
3 changes: 3 additions & 0 deletions fxevent/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ type Provided struct {

// Err is non-nil if we failed to provide this constructor.
Err error

// Private denotes whether the provided constructor is a [Private] constructor.
Private bool
}

// Replaced is emitted when a value replaces a type in Fx.
Expand Down
8 changes: 8 additions & 0 deletions fxevent/zap.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func (l *ZapLogger) LogEvent(event Event) {
zap.String("constructor", e.ConstructorName),
moduleField(e.ModuleName),
zap.String("type", rtype),
maybeBool("private", e.Private),
)
}
if e.Err != nil {
Expand Down Expand Up @@ -199,3 +200,10 @@ func moduleField(name string) zap.Field {
}
return zap.String("module", name)
}

func maybeBool(name string, b bool) zap.Field {
if b {
return zap.Bool(name, true)
}
return zap.Skip()
}
17 changes: 17 additions & 0 deletions fxevent/zap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func TestZapLogger(t *testing.T) {
ConstructorName: "bytes.NewBuffer()",
ModuleName: "myModule",
OutputTypeNames: []string{"*bytes.Buffer"},
Private: false,
},
wantMessage: "provided",
wantFields: map[string]interface{}{
Expand All @@ -159,6 +160,22 @@ func TestZapLogger(t *testing.T) {
"module": "myModule",
},
},
{
name: "PrivateProvide",
give: &Provided{
ConstructorName: "bytes.NewBuffer()",
ModuleName: "myModule",
OutputTypeNames: []string{"*bytes.Buffer"},
Private: true,
},
wantMessage: "provided",
wantFields: map[string]interface{}{
"constructor": "bytes.NewBuffer()",
"type": "*bytes.Buffer",
"module": "myModule",
"private": true,
},
},
{
name: "Provide/Error",
give: &Provided{Err: someError},
Expand Down
3 changes: 2 additions & 1 deletion module.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (m *module) provide(p provide) {
}

var info dig.ProvideInfo
if err := runProvide(m.scope, p, dig.FillProvideInfo(&info), dig.Export(true)); err != nil {
if err := runProvide(m.scope, p, dig.FillProvideInfo(&info), dig.Export(!p.Private)); err != nil {
m.app.err = err
}
var ev fxevent.Event
Expand All @@ -170,6 +170,7 @@ func (m *module) provide(p provide) {
ModuleName: m.name,
OutputTypeNames: outputNames,
Err: m.app.err,
Private: p.Private,
}
}
m.log.LogEvent(ev)
Expand Down
Loading

0 comments on commit 6d76d64

Please sign in to comment.