Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,14 @@ Cycles are detected at build time and return a `ProviderCycleError`.

## Controllers

Controllers are built after providers and returned in `App.Controllers`:
Controllers are built after providers and returned in `App.Controllers`. Keys are namespaced as `module:controller`:

```go
app, _ := kernel.Bootstrap(&AppModule{})

// Controllers are ready to use
for name, controller := range app.Controllers {
fmt.Println(name) // e.g., "UsersController"
fmt.Println(name) // e.g., "users:UsersController"
}
```

Expand Down
20 changes: 12 additions & 8 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,28 @@ Entry point for bootstrapping your application. Returns an `App` with built cont
```go
type App struct {
Controllers map[string]any
Container Container
}
```

| Field | Description |
|-------|-------------|
| `Controllers` | Map of controller name → controller instance |
| `Container` | Access to the provider container |
| `Controllers` | Map of controller key (`module:controller`) → controller instance |

### Container
### App.Get

```go
type Container interface {
Get(token Token) (any, error)
}
func (a *App) Get(token Token) (any, error)
```

Resolves a token from the root module scope.

### App.Resolver

```go
func (a *App) Resolver() Resolver
```

Provides access to built providers. Used for manual resolution or cleanup.
Returns a root-scoped resolver that enforces module visibility.

### Errors

Expand Down
15 changes: 12 additions & 3 deletions modkit/kernel/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ type App struct {
Controllers map[string]any
}

func controllerKey(moduleName, controllerName string) string {
return moduleName + ":" + controllerName
}

func Bootstrap(root module.Module) (*App, error) {
graph, err := BuildGraph(root)
if err != nil {
Expand All @@ -25,17 +29,22 @@ func Bootstrap(root module.Module) (*App, error) {
}

controllers := make(map[string]any)
perModule := make(map[string]map[string]bool)
for _, node := range graph.Modules {
if perModule[node.Name] == nil {
perModule[node.Name] = make(map[string]bool)
}
resolver := container.resolverFor(node.Name)
for _, controller := range node.Def.Controllers {
if _, exists := controllers[controller.Name]; exists {
return nil, &DuplicateControllerNameError{Name: controller.Name}
if perModule[node.Name][controller.Name] {
return nil, &DuplicateControllerNameError{Module: node.Name, Name: controller.Name}
}
perModule[node.Name][controller.Name] = true
instance, err := controller.Build(resolver)
if err != nil {
return nil, &ControllerBuildError{Module: node.Name, Controller: controller.Name, Err: err}
}
controllers[controller.Name] = instance
controllers[controllerKey(node.Name, controller.Name)] = instance
}
}

Expand Down
112 changes: 109 additions & 3 deletions modkit/kernel/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ func TestBootstrapAllowsReExportedTokens(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

if app.Controllers["UsesShared"] != "value" {
t.Fatalf("unexpected controller value: %v", app.Controllers["UsesShared"])
if app.Controllers["A:UsesShared"] != "value" {
t.Fatalf("unexpected controller value: %v", app.Controllers["A:UsesShared"])
}
}

Expand Down Expand Up @@ -171,6 +171,9 @@ func TestBootstrapRejectsDuplicateControllerNames(t *testing.T) {
if dupErr.Name != "ControllerA" {
t.Fatalf("unexpected controller name: %q", dupErr.Name)
}
if dupErr.Module != "A" {
t.Fatalf("unexpected module name: %q", dupErr.Module)
}
}

func TestBootstrapRegistersControllers(t *testing.T) {
Expand All @@ -189,7 +192,110 @@ func TestBootstrapRegistersControllers(t *testing.T) {
t.Fatalf("Bootstrap failed: %v", err)
}

if app.Controllers["ControllerA"] != "controller" {
if app.Controllers["A:ControllerA"] != "controller" {
t.Fatalf("expected controller instance to be registered")
}
}

func TestBootstrapAllowsSameControllerNameAcrossModules(t *testing.T) {
modB := mod("B", nil, nil,
[]module.ControllerDef{{
Name: "Shared",
Build: func(r module.Resolver) (any, error) {
return "controller-from-B", nil
},
}},
nil,
)

modA := mod("A", []module.Module{modB}, nil,
[]module.ControllerDef{{
Name: "Shared",
Build: func(r module.Resolver) (any, error) {
return "controller-from-A", nil
},
}},
nil,
)

app, err := kernel.Bootstrap(modA)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

if len(app.Controllers) != 2 {
t.Fatalf("expected 2 controllers, got %d", len(app.Controllers))
}

if app.Controllers["A:Shared"] != "controller-from-A" {
t.Errorf("controller A:Shared has wrong value: %v", app.Controllers["A:Shared"])
}
if app.Controllers["B:Shared"] != "controller-from-B" {
t.Errorf("controller B:Shared has wrong value: %v", app.Controllers["B:Shared"])
}
}

func TestBootstrapRejectsDuplicateControllerInSameModule(t *testing.T) {
modA := mod("A", nil, nil,
[]module.ControllerDef{
{
Name: "Dup",
Build: func(r module.Resolver) (any, error) {
return "one", nil
},
},
{
Name: "Dup",
Build: func(r module.Resolver) (any, error) {
return "two", nil
},
},
},
nil,
)

_, err := kernel.Bootstrap(modA)
if err == nil {
t.Fatalf("expected duplicate controller error")
}

var dupErr *kernel.DuplicateControllerNameError
if !errors.As(err, &dupErr) {
t.Fatalf("unexpected error type: %T", err)
}
if dupErr.Name != "Dup" {
t.Fatalf("unexpected controller name: %q", dupErr.Name)
}
if dupErr.Module != "A" {
t.Fatalf("unexpected module name: %q", dupErr.Module)
}
}

func TestControllerKeyFormat(t *testing.T) {
modA := mod("users", nil, nil,
[]module.ControllerDef{{
Name: "Controller",
Build: func(r module.Resolver) (any, error) {
return nil, nil
},
}},
nil,
)

app, err := kernel.Bootstrap(modA)
if err != nil {
t.Fatalf("Bootstrap failed: %v", err)
}

if _, ok := app.Controllers["users:Controller"]; !ok {
t.Fatalf("expected key 'users:Controller', got keys: %v", controllerKeys(app.Controllers))
}
}

func controllerKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
}
5 changes: 3 additions & 2 deletions modkit/kernel/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@ func (e *DuplicateProviderTokenError) Error() string {
}

type DuplicateControllerNameError struct {
Name string
Module string
Name string
}

func (e *DuplicateControllerNameError) Error() string {
return fmt.Sprintf("duplicate controller name: %s", e.Name)
return fmt.Sprintf("duplicate controller name in module %q: %s", e.Module, e.Name)
}

type TokenNotVisibleError struct {
Expand Down
Loading