Files
OpenAppLock/Shared/RuleConfiguration.swift
Brendan Chen 783a8571a7 refactor: model rule options as a per-kind sum type
Replace the wide BlockingRule/RuleDraft "god struct" — where every option
existed for every kind — with a RuleConfiguration sum type that carries only
the options each kind actually has:

  .schedule(ScheduleConfig: window + selectionMode + blockAdultContent)
  .timeLimit(TimeLimitConfig: dailyLimitMinutes)
  .openLimit(OpenLimitConfig: maxOpens)

Name, days, Hard Mode, app list, and pause stay common to all kinds. This
makes illegal states unrepresentable: Block / Allow Only and Block Adult
Content are now structurally Schedule-only.

User-visible behavior change: the Time Limit and Open Limit editors no longer
offer a Block Adult Content toggle, and their detail sheets drop the "Adult
websites" row — those never made sense for a usage budget. Limit rules are
always Block and never engage the web-content filter.

BlockingRule keeps flat columns as raw persistence behind a computed
`configuration` bridge (lowest SwiftData risk; the cross-process RuleSnapshot
wire format and the Screen Time extensions are untouched). Logic, the editors,
and the detail sheet all switch on the sum type.

Spec (§1, §3.5/§3.6, §5.2) updated first; tests reworked to the new API with
new structural-guarantee unit tests and a UI test asserting the Time Limit
editor/detail omit adult content. Full suite green (180 tests).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 12:51:26 -04:00

91 lines
2.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// RuleConfiguration.swift
// OpenAppLock
//
import Foundation
/// The kind-specific options of a rule, modelled as a sum type so a rule can
/// only carry the options that belong to its kind. This makes illegal states
/// unrepresentable: an Open Limit rule cannot hold a time window, a Time Limit
/// rule cannot hold a Block/Allow-Only mode, and neither limit kind can hold
/// Block Adult Content those are Schedule-only options.
///
/// Kind-common attributes (name, days, hardMode, isEnabled, appList,
/// pausedUntil) live on the owning `BlockingRule` / `RuleDraft`, not here.
enum RuleConfiguration: Hashable, Sendable {
case schedule(ScheduleConfig)
case timeLimit(TimeLimitConfig)
case openLimit(OpenLimitConfig)
var kind: RuleKind {
switch self {
case .schedule: .schedule
case .timeLimit: .timeLimit
case .openLimit: .openLimit
}
}
/// The default configuration for a brand-new rule of the given kind, using
/// the reference app's defaults (95 schedule, 45m/day, 5 opens/day).
static func `default`(for kind: RuleKind) -> RuleConfiguration {
switch kind {
case .schedule: .schedule(ScheduleConfig())
case .timeLimit: .timeLimit(TimeLimitConfig())
case .openLimit: .openLimit(OpenLimitConfig())
}
}
var scheduleConfig: ScheduleConfig? {
if case .schedule(let config) = self { config } else { nil }
}
var timeLimitConfig: TimeLimitConfig? {
if case .timeLimit(let config) = self { config } else { nil }
}
var openLimitConfig: OpenLimitConfig? {
if case .openLimit(let config) = self { config } else { nil }
}
}
/// Schedule-rule options: a recurring time window, how the app list is
/// interpreted, and whether the adult-website filter engages while the window
/// is active. A window whose end is at or before its start crosses midnight.
struct ScheduleConfig: Hashable, Sendable {
var startMinutes: Int
var endMinutes: Int
var selectionMode: SelectionMode
var blockAdultContent: Bool
init(
startMinutes: Int = 9 * 60,
endMinutes: Int = 17 * 60,
selectionMode: SelectionMode = .block,
blockAdultContent: Bool = false
) {
self.startMinutes = startMinutes
self.endMinutes = endMinutes
self.selectionMode = selectionMode
self.blockAdultContent = blockAdultContent
}
}
/// Time Limit option: a daily cumulative-usage budget in minutes.
struct TimeLimitConfig: Hashable, Sendable {
var dailyLimitMinutes: Int
init(dailyLimitMinutes: Int = 45) {
self.dailyLimitMinutes = dailyLimitMinutes
}
}
/// Open Limit option: a daily budget of app opens.
struct OpenLimitConfig: Hashable, Sendable {
var maxOpens: Int
init(maxOpens: Int = 5) {
self.maxOpens = maxOpens
}
}