Skip to content

Commit d057810

Browse files
committed
Add Wire-based DI for server wiring
1 parent eea69b0 commit d057810

6 files changed

Lines changed: 282 additions & 125 deletions

File tree

cmd/gamifykit-server/app.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"net/http"
8+
"os"
9+
10+
mem "gamifykit/adapters/memory"
11+
redisAdapter "gamifykit/adapters/redis"
12+
sqlxAdapter "gamifykit/adapters/sqlx"
13+
"gamifykit/api/httpapi"
14+
"gamifykit/config"
15+
"gamifykit/engine"
16+
"gamifykit/gamify"
17+
"gamifykit/realtime"
18+
)
19+
20+
// App aggregates the assembled server components.
21+
type App struct {
22+
Config *config.Config
23+
Logger *slog.Logger
24+
Hub *realtime.Hub
25+
Service *engine.GamifyService
26+
Handler http.Handler
27+
Server *http.Server
28+
}
29+
30+
func provideConfig(ctx context.Context) (*config.Config, error) {
31+
cfg, err := config.Load()
32+
if err != nil {
33+
return nil, err
34+
}
35+
if cfg.Environment == config.EnvProduction {
36+
if err := cfg.LoadSecretsFromEnv(ctx); err != nil {
37+
return nil, err
38+
}
39+
}
40+
return cfg, nil
41+
}
42+
43+
func provideLogger(cfg *config.Config) *slog.Logger {
44+
return setupLogging(cfg)
45+
}
46+
47+
func provideHub() *realtime.Hub {
48+
return realtime.NewHub()
49+
}
50+
51+
func provideStorage(ctx context.Context, cfg *config.Config) (engine.Storage, error) {
52+
return setupStorage(ctx, cfg)
53+
}
54+
55+
func provideService(hub *realtime.Hub, storage engine.Storage) *engine.GamifyService {
56+
return gamify.New(
57+
gamify.WithRealtime(hub),
58+
gamify.WithStorage(storage),
59+
gamify.WithDispatchMode(engine.DispatchAsync),
60+
)
61+
}
62+
63+
func provideHandler(svc *engine.GamifyService, hub *realtime.Hub, cfg *config.Config) http.Handler {
64+
return httpapi.NewMux(svc, hub, httpapi.Options{
65+
PathPrefix: cfg.Server.PathPrefix,
66+
AllowCORSOrigin: cfg.Server.CORSOrigin,
67+
})
68+
}
69+
70+
func provideServer(cfg *config.Config, handler http.Handler) *http.Server {
71+
return &http.Server{
72+
Addr: cfg.Server.Address,
73+
Handler: handler,
74+
ReadHeaderTimeout: cfg.Server.ReadHeaderTimeout,
75+
ReadTimeout: cfg.Server.ReadTimeout,
76+
WriteTimeout: cfg.Server.WriteTimeout,
77+
IdleTimeout: cfg.Server.IdleTimeout,
78+
}
79+
}
80+
81+
// setupLogging configures the logger based on configuration.
82+
func setupLogging(cfg *config.Config) *slog.Logger {
83+
var handler slog.Handler
84+
85+
opts := &slog.HandlerOptions{
86+
Level: parseLogLevel(cfg.Logging.Level),
87+
}
88+
89+
switch cfg.Logging.Format {
90+
case "text":
91+
handler = slog.NewTextHandler(os.Stdout, opts)
92+
case "json":
93+
handler = slog.NewJSONHandler(os.Stdout, opts)
94+
default:
95+
handler = slog.NewJSONHandler(os.Stdout, opts)
96+
}
97+
98+
if len(cfg.Logging.Attributes) > 0 {
99+
handler = handler.WithAttrs(convertAttributes(cfg.Logging.Attributes))
100+
}
101+
102+
logger := slog.New(handler)
103+
slog.SetDefault(logger)
104+
return logger
105+
}
106+
107+
// parseLogLevel converts string log level to slog.Level.
108+
func parseLogLevel(level string) slog.Level {
109+
switch level {
110+
case "debug":
111+
return slog.LevelDebug
112+
case "info":
113+
return slog.LevelInfo
114+
case "warn":
115+
return slog.LevelWarn
116+
case "error":
117+
return slog.LevelError
118+
default:
119+
return slog.LevelInfo
120+
}
121+
}
122+
123+
// convertAttributes converts map[string]string to []slog.Attr.
124+
func convertAttributes(attrs map[string]string) []slog.Attr {
125+
var result []slog.Attr
126+
for k, v := range attrs {
127+
result = append(result, slog.String(k, v))
128+
}
129+
return result
130+
}
131+
132+
// setupStorage creates the appropriate storage adapter based on configuration.
133+
func setupStorage(ctx context.Context, cfg *config.Config) (engine.Storage, error) {
134+
switch cfg.Storage.Adapter {
135+
case "memory":
136+
return mem.New(), nil
137+
case "redis":
138+
return redisAdapter.New(cfg.Storage.Redis)
139+
case "sql":
140+
return sqlxAdapter.New(cfg.Storage.SQL)
141+
case "file":
142+
return mem.New(), fmt.Errorf("file storage not yet implemented, using memory fallback")
143+
default:
144+
return mem.New(), fmt.Errorf("unknown storage adapter: %s", cfg.Storage.Adapter)
145+
}
146+
}

cmd/gamifykit-server/main.go

Lines changed: 9 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -8,78 +8,33 @@ import (
88
"os"
99
"os/signal"
1010
"syscall"
11-
12-
mem "gamifykit/adapters/memory"
13-
redisAdapter "gamifykit/adapters/redis"
14-
sqlxAdapter "gamifykit/adapters/sqlx"
15-
"gamifykit/api/httpapi"
16-
"gamifykit/config"
17-
"gamifykit/engine"
18-
"gamifykit/gamify"
19-
"gamifykit/realtime"
2011
)
2112

2213
func main() {
23-
// Load configuration
24-
cfg, err := config.Load()
14+
ctx := context.Background()
15+
app, err := BuildApp(ctx)
2516
if err != nil {
26-
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
17+
fmt.Fprintf(os.Stderr, "Failed to initialize app: %v\n", err)
2718
os.Exit(1)
2819
}
2920

30-
// Setup logging based on configuration
31-
setupLogging(cfg)
32-
33-
// Load secrets if in production
34-
ctx := context.Background()
35-
if cfg.Environment == config.EnvProduction {
36-
if err := cfg.LoadSecretsFromEnv(ctx); err != nil {
37-
slog.Error("Failed to load secrets", "error", err)
38-
os.Exit(1)
39-
}
40-
}
21+
cfg := app.Config
4122

4223
slog.Info("starting gamifykit server",
4324
"environment", cfg.Environment,
4425
"profile", cfg.Profile,
4526
"address", cfg.Server.Address,
4627
"storage_adapter", cfg.Storage.Adapter)
4728

48-
// Setup storage adapter
49-
storage, err := setupStorage(ctx, cfg)
50-
if err != nil {
51-
slog.Error("Failed to setup storage", "error", err)
52-
os.Exit(1)
53-
}
54-
55-
// Build service
56-
hub := realtime.NewHub()
57-
svc := gamify.New(
58-
gamify.WithRealtime(hub),
59-
gamify.WithStorage(storage),
60-
gamify.WithDispatchMode(engine.DispatchAsync),
61-
)
62-
63-
// Setup HTTP API
64-
handler := httpapi.NewMux(svc, hub, httpapi.Options{
65-
PathPrefix: cfg.Server.PathPrefix,
66-
AllowCORSOrigin: cfg.Server.CORSOrigin,
67-
})
68-
69-
// Create HTTP server
70-
srv := &http.Server{
71-
Addr: cfg.Server.Address,
72-
Handler: handler,
73-
ReadHeaderTimeout: cfg.Server.ReadHeaderTimeout,
74-
ReadTimeout: cfg.Server.ReadTimeout,
75-
WriteTimeout: cfg.Server.WriteTimeout,
76-
IdleTimeout: cfg.Server.IdleTimeout,
77-
}
29+
srv := app.Server
7830

7931
// Start server in a goroutine
8032
go func() {
8133
slog.Info("server listening", "address", cfg.Server.Address)
82-
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
34+
if err := srv.ListenAndServe(); err != nil {
35+
if err == http.ErrServerClosed {
36+
return
37+
}
8338
slog.Error("failed to start server", "error", err)
8439
os.Exit(1)
8540
}
@@ -102,73 +57,3 @@ func main() {
10257

10358
slog.Info("server stopped")
10459
}
105-
106-
// setupLogging configures the logger based on configuration
107-
func setupLogging(cfg *config.Config) {
108-
var handler slog.Handler
109-
110-
opts := &slog.HandlerOptions{
111-
Level: parseLogLevel(cfg.Logging.Level),
112-
}
113-
114-
switch cfg.Logging.Format {
115-
case "text":
116-
handler = slog.NewTextHandler(os.Stdout, opts)
117-
case "json":
118-
handler = slog.NewJSONHandler(os.Stdout, opts)
119-
default:
120-
handler = slog.NewJSONHandler(os.Stdout, opts)
121-
}
122-
123-
// Add attributes if specified
124-
if len(cfg.Logging.Attributes) > 0 {
125-
handler = handler.WithAttrs(convertAttributes(cfg.Logging.Attributes))
126-
}
127-
128-
slog.SetDefault(slog.New(handler))
129-
}
130-
131-
// parseLogLevel converts string log level to slog.Level
132-
func parseLogLevel(level string) slog.Level {
133-
switch level {
134-
case "debug":
135-
return slog.LevelDebug
136-
case "info":
137-
return slog.LevelInfo
138-
case "warn":
139-
return slog.LevelWarn
140-
case "error":
141-
return slog.LevelError
142-
default:
143-
return slog.LevelInfo
144-
}
145-
}
146-
147-
// convertAttributes converts map[string]string to []slog.Attr
148-
func convertAttributes(attrs map[string]string) []slog.Attr {
149-
var result []slog.Attr
150-
for k, v := range attrs {
151-
result = append(result, slog.String(k, v))
152-
}
153-
return result
154-
}
155-
156-
// setupStorage creates the appropriate storage adapter based on configuration
157-
func setupStorage(ctx context.Context, cfg *config.Config) (engine.Storage, error) {
158-
switch cfg.Storage.Adapter {
159-
case "memory":
160-
return mem.New(), nil
161-
162-
case "redis":
163-
return redisAdapter.New(cfg.Storage.Redis)
164-
165-
case "sql":
166-
return sqlxAdapter.New(cfg.Storage.SQL)
167-
168-
case "file":
169-
return mem.New(), fmt.Errorf("file storage not yet implemented, using memory fallback")
170-
171-
default:
172-
return mem.New(), fmt.Errorf("unknown storage adapter: %s", cfg.Storage.Adapter)
173-
}
174-
}

cmd/gamifykit-server/wire.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build wireinject
2+
// +build wireinject
3+
4+
package main
5+
6+
import (
7+
"context"
8+
9+
"github.com/google/wire"
10+
)
11+
12+
// BuildApp wires the server components using Google Wire.
13+
func BuildApp(ctx context.Context) (*App, error) {
14+
wire.Build(
15+
provideConfig,
16+
provideLogger,
17+
provideHub,
18+
provideStorage,
19+
provideService,
20+
provideHandler,
21+
provideServer,
22+
wire.Struct(new(App), "*"),
23+
)
24+
return nil, nil
25+
}

cmd/gamifykit-server/wire_gen.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ require (
1414
github.com/davecgh/go-spew v1.1.1 // indirect
1515
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
1616
github.com/go-sql-driver/mysql v1.9.3 // indirect
17+
github.com/google/subcommands v1.2.0 // indirect
18+
github.com/google/wire v0.6.0 // indirect
1719
github.com/jmoiron/sqlx v1.4.0 // indirect
1820
github.com/lib/pq v1.10.9 // indirect
1921
github.com/pmezard/go-difflib v1.0.0 // indirect
20-
golang.org/x/net v0.17.0 // indirect
22+
golang.org/x/mod v0.14.0 // indirect
23+
golang.org/x/net v0.20.0 // indirect
24+
golang.org/x/tools v0.17.0 // indirect
2125
gopkg.in/yaml.v3 v3.0.1 // indirect
2226
)

0 commit comments

Comments
 (0)