From d7f8fb49eb93fe6e4ff86c608ce71d2377eac4f7 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Tue, 22 Apr 2025 16:06:27 +0200 Subject: [PATCH] refactor(app): add App type --- internal/app/app.go | 68 ++++++++++++++++-------- internal/app/integration_helpers_test.go | 4 +- internal/app/integration_test.go | 25 +++++---- main.go | 29 +++++----- 4 files changed, 73 insertions(+), 53 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index b3c86ee..8e80b65 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -18,8 +18,19 @@ import ( "github.com/docker/docker/client" ) -// RunParams holds the parameters for running the application. -type RunParams struct { +// App is an instance of the app. +type App struct { + configService *config.Service + dockerClient container.DockerClient + screen *terminal.Screen // Screen may be nil. + clipboardAvailable bool + configFilePath string + buildInfo domain.BuildInfo + logger *slog.Logger +} + +// Params holds the parameters for running the application. +type Params struct { ConfigService *config.Service DockerClient container.DockerClient Screen *terminal.Screen // Screen may be nil. @@ -29,14 +40,25 @@ type RunParams struct { Logger *slog.Logger } +func New(params Params) *App { + return &App{ + configService: params.ConfigService, + dockerClient: params.DockerClient, + screen: params.Screen, + clipboardAvailable: params.ClipboardAvailable, + configFilePath: params.ConfigFilePath, + buildInfo: params.BuildInfo, + logger: params.Logger, + } +} + // Run starts the application, and blocks until it exits. -func Run(ctx context.Context, params RunParams) error { - logger := params.Logger - eventBus := event.NewBus(logger.With("component", "event_bus")) +func (a *App) Run(ctx context.Context) error { + eventBus := event.NewBus(a.logger.With("component", "event_bus")) // cfg is the current configuration of the application, as reflected in the // config file. - cfg := params.ConfigService.Current() + cfg := a.configService.Current() // state is the current state of the application, as reflected in the UI. state := new(domain.AppState) @@ -49,11 +71,11 @@ func Run(ctx context.Context, params RunParams) error { ui, err := terminal.StartUI(ctx, terminal.StartParams{ EventBus: eventBus, - Screen: params.Screen, - ClipboardAvailable: params.ClipboardAvailable, - ConfigFilePath: params.ConfigFilePath, - BuildInfo: params.BuildInfo, - Logger: logger.With("component", "ui"), + Screen: a.screen, + ClipboardAvailable: a.clipboardAvailable, + ConfigFilePath: a.configFilePath, + BuildInfo: a.buildInfo, + Logger: a.logger.With("component", "ui"), }) if err != nil { return fmt.Errorf("start terminal user interface: %w", err) @@ -71,7 +93,7 @@ func Run(ctx context.Context, params RunParams) error { // non-test code is pretty low. emptyUI := func() { ui.SetState(domain.AppState{}) } - containerClient, err := container.NewClient(ctx, params.DockerClient, logger.With("component", "container_client")) + containerClient, err := container.NewClient(ctx, a.dockerClient, a.logger.With("component", "container_client")) if err != nil { err = fmt.Errorf("create container client: %w", err) @@ -106,7 +128,7 @@ func Run(ctx context.Context, params RunParams) error { TLSKeyPath: tlsKeyPath, StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey), ContainerClient: containerClient, - Logger: logger.With("component", "mediaserver"), + Logger: a.logger.With("component", "mediaserver"), }) if err != nil { err = fmt.Errorf("create mediaserver: %w", err) @@ -120,7 +142,7 @@ func Run(ctx context.Context, params RunParams) error { repl := replicator.StartActor(ctx, replicator.StartActorParams{ SourceURL: srv.RTMPInternalURL(), ContainerClient: containerClient, - Logger: logger.With("component", "replicator"), + Logger: a.logger.With("component", "replicator"), }) defer repl.Close() @@ -146,16 +168,16 @@ func Run(ctx context.Context, params RunParams) error { eventBus.Send(event.MediaServerStartedEvent{RTMPURL: srv.RTMPURL(), RTMPSURL: srv.RTMPSURL()}) } - case <-params.ConfigService.C(): + case <-a.configService.C(): // No-op, config updates are handled synchronously for now. case cmd, ok := <-ui.C(): if !ok { // TODO: keep UI open until all containers have closed - logger.Info("UI closed") + a.logger.Info("UI closed") return nil } - logger.Debug("Command received", "cmd", cmd.Name()) + a.logger.Debug("Command received", "cmd", cmd.Name()) switch c := cmd.(type) { case domain.CommandAddDestination: newCfg := cfg @@ -163,8 +185,8 @@ func Run(ctx context.Context, params RunParams) error { Name: c.DestinationName, URL: c.URL, }) - if err := params.ConfigService.SetConfig(newCfg); err != nil { - logger.Error("Config update failed", "err", err) + if err := a.configService.SetConfig(newCfg); err != nil { + a.logger.Error("Config update failed", "err", err) ui.ConfigUpdateFailed(err) continue } @@ -177,8 +199,8 @@ func Run(ctx context.Context, params RunParams) error { newCfg.Destinations = slices.DeleteFunc(newCfg.Destinations, func(dest config.Destination) bool { return dest.URL == c.URL }) - if err := params.ConfigService.SetConfig(newCfg); err != nil { - logger.Error("Config update failed", "err", err) + if err := a.configService.SetConfig(newCfg); err != nil { + a.logger.Error("Config update failed", "err", err) ui.ConfigUpdateFailed(err) continue } @@ -200,11 +222,11 @@ func Run(ctx context.Context, params RunParams) error { case <-uiUpdateT.C: updateUI() case serverState := <-srv.C(): - logger.Debug("Server state received", "state", serverState) + a.logger.Debug("Server state received", "state", serverState) applyServerState(serverState, state) updateUI() case replState := <-repl.C(): - logger.Debug("Replicator state received", "state", replState) + a.logger.Debug("Replicator state received", "state", replState) destErrors := applyReplicatorState(replState, state) for _, destError := range destErrors { diff --git a/internal/app/integration_helpers_test.go b/internal/app/integration_helpers_test.go index be288b0..7ac4898 100644 --- a/internal/app/integration_helpers_test.go +++ b/internal/app/integration_helpers_test.go @@ -31,10 +31,10 @@ func buildAppParams( screen tcell.SimulationScreen, screenCaptureC chan<- terminal.ScreenCapture, logger *slog.Logger, -) app.RunParams { +) app.Params { t.Helper() - return app.RunParams{ + return app.Params{ ConfigService: configService, DockerClient: dockerClient, Screen: &terminal.Screen{ diff --git a/internal/app/integration_test.go b/internal/app/integration_test.go index 9b47d3f..84a894f 100644 --- a/internal/app/integration_test.go +++ b/internal/app/integration_test.go @@ -132,7 +132,7 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) { done <- struct{}{} }() - err := app.Run(ctx, app.RunParams{ + require.NoError(t, app.New(app.Params{ ConfigService: configService, DockerClient: dockerClient, Screen: &terminal.Screen{ @@ -144,8 +144,7 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) { ClipboardAvailable: false, BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"}, Logger: logger, - }) - require.NoError(t, err) + }).Run(ctx)) }() require.EventuallyWithT( @@ -320,7 +319,7 @@ func TestIntegrationCustomHost(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() time.Sleep(time.Second) @@ -391,7 +390,7 @@ func TestIntegrationCustomTLSCerts(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() require.EventuallyWithT( @@ -472,7 +471,7 @@ func TestIntegrationRestartDestination(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() require.EventuallyWithT( @@ -609,7 +608,7 @@ func TestIntegrationStartDestinationFailed(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() require.EventuallyWithT( @@ -682,7 +681,7 @@ func TestIntegrationDestinationValidations(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() require.EventuallyWithT( @@ -824,7 +823,7 @@ func TestIntegrationStartupCheck(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() require.EventuallyWithT( @@ -893,7 +892,7 @@ func TestIntegrationMediaServerError(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() require.EventuallyWithT( @@ -934,7 +933,7 @@ func TestIntegrationDockerClientError(t *testing.T) { require.EqualError( t, - app.Run(ctx, buildAppParams(t, configService, &dockerClient, screen, screenCaptureC, logger)), + app.New(buildAppParams(t, configService, &dockerClient, screen, screenCaptureC, logger)).Run(ctx), "create container client: network create: boom", ) }() @@ -974,7 +973,7 @@ func TestIntegrationDockerConnectionError(t *testing.T) { done <- struct{}{} }() - err := app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)) + err := app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx) require.ErrorContains(t, err, "dial tcp: lookup docker.example.com") require.ErrorContains(t, err, "no such host") }() @@ -1070,7 +1069,7 @@ func TestIntegrationCopyURLs(t *testing.T) { done <- struct{}{} }() - require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)) }() time.Sleep(3 * time.Second) diff --git a/main.go b/main.go index 5f07d37..23420a5 100644 --- a/main.go +++ b/main.go @@ -97,22 +97,21 @@ func run(ctx context.Context) error { return fmt.Errorf("read build info: %w", err) } - return app.Run( - ctx, - app.RunParams{ - ConfigService: configService, - DockerClient: dockerClient, - ClipboardAvailable: clipboardAvailable, - ConfigFilePath: configService.Path(), - BuildInfo: domain.BuildInfo{ - GoVersion: buildInfo.GoVersion, - Version: version, - Commit: commit, - Date: date, - }, - Logger: logger, + app := app.New(app.Params{ + ConfigService: configService, + DockerClient: dockerClient, + ClipboardAvailable: clipboardAvailable, + ConfigFilePath: configService.Path(), + BuildInfo: domain.BuildInfo{ + GoVersion: buildInfo.GoVersion, + Version: version, + Commit: commit, + Date: date, }, - ) + Logger: logger, + }) + + return app.Run(ctx) } // editConfigFile opens the config file in the user's editor.