From e65c780b6d54018c98430488790b2473008aaf81 Mon Sep 17 00:00:00 2001 From: Fritz Date: Sun, 10 May 2026 09:51:23 -0700 Subject: [PATCH] refactor: clean up SessionPhase after code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move forward-compat fallback into SessionPhase.init(from:) so callers use typed decodeIfPresent instead of a String→rawValue detour - Guard pause()/resume() before writing to avoid spurious @Observable notifications when phase is already in the target state - Remove duplicate // MARK: Lifecycle property marker (methods section already carries the canonical MARK) - Replace force-unwrap in tests with try #require per project convention --- Sources/DHKit/EncounterSession.swift | 17 +++++++++++------ Tests/DHKitTests/EncounterSessionTests.swift | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/DHKit/EncounterSession.swift b/Sources/DHKit/EncounterSession.swift index e25b5bd..6f11d7b 100644 --- a/Sources/DHKit/EncounterSession.swift +++ b/Sources/DHKit/EncounterSession.swift @@ -28,11 +28,18 @@ import Observation // MARK: - SessionPhase /// The lifecycle phase of a live encounter session. -public enum SessionPhase: String, Codable, Sendable { +public enum SessionPhase: String, Sendable { case running case paused } +extension SessionPhase: Codable { + public init(from decoder: any Decoder) throws { + let raw = try decoder.singleValueContainer().decode(String.self) + self = SessionPhase(rawValue: raw) ?? .running + } +} + // MARK: - EncounterSession /// The live state of a Daggerheart encounter being run at the table. @@ -108,8 +115,6 @@ public final class EncounterSession: Identifiable, Hashable { /// Running total of spotlight grants in this encounter. public var spotlightCount: Int - // MARK: Lifecycle - /// The current lifecycle phase of this session. public private(set) var phase: SessionPhase @@ -158,11 +163,13 @@ public final class EncounterSession: Identifiable, Hashable { /// Pause this session. The session persists and can be resumed later. public func pause() { + guard phase != .paused else { return } phase = .paused } /// Resume a paused session. public func resume() { + guard phase != .running else { return } phase = .running } @@ -513,9 +520,7 @@ extension EncounterSession: @MainActor Codable { let spotlightedSlotID = try c.decodeIfPresent(UUID.self, forKey: .spotlightedSlotID) let spotlightCount = try c.decode(Int.self, forKey: .spotlightCount) let gmNotes = try c.decode(String.self, forKey: .gmNotes) - let phase = - (try c.decodeIfPresent(String.self, forKey: .phase)) - .flatMap(SessionPhase.init(rawValue:)) ?? .running + let phase = try c.decodeIfPresent(SessionPhase.self, forKey: .phase) ?? .running let definitionID = try c.decodeIfPresent(UUID.self, forKey: .definitionID) let definitionSnapshotDate = try c.decodeIfPresent(Date.self, forKey: .definitionSnapshotDate) self.init( diff --git a/Tests/DHKitTests/EncounterSessionTests.swift b/Tests/DHKitTests/EncounterSessionTests.swift index 86a2fd9..edb3417 100644 --- a/Tests/DHKitTests/EncounterSessionTests.swift +++ b/Tests/DHKitTests/EncounterSessionTests.swift @@ -663,7 +663,7 @@ import Testing {"id":"\(UUID().uuidString)","name":"Legacy","adversarySlots":[],"playerSlots":[],\ "environmentSlots":[],"fearPool":0,"hopePool":0,"spotlightCount":0,"gmNotes":""} """ - let data = json.data(using: .utf8)! + let data = try #require(json.data(using: .utf8)) let decoded = try JSONDecoder().decode(EncounterSession.self, from: data) #expect(decoded.phase == .running) } @@ -676,7 +676,7 @@ import Testing "environmentSlots":[],"fearPool":0,"hopePool":0,"spotlightCount":0,\ "gmNotes":"","phase":"completed"} """ - let data = json.data(using: .utf8)! + let data = try #require(json.data(using: .utf8)) let decoded = try JSONDecoder().decode(EncounterSession.self, from: data) #expect(decoded.phase == .running) }