Files
OpenAppLock/OpenAppLock/Services/SampleRules.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

98 lines
3.9 KiB
Swift

//
// SampleRules.swift
// OpenAppLock
//
import Foundation
import SwiftData
/// Builds deterministic rules for UI-test scenarios, positioned relative to
/// "now" so an active window is genuinely active whenever the test runs.
enum SampleRules {
static func seed(
_ scenario: LaunchConfiguration.SeedScenario,
into context: ModelContext,
usage: MockUsageLedger? = nil,
now: Date = .now,
calendar: Calendar = .current
) {
// Shared list so seeded rules demonstrate the app-list UI. The count
// is display-only; UI tests never decode real tokens.
let distractions = AppList(name: "Distractions", selectionCount: 3)
context.insert(distractions)
let rules: [BlockingRule]
switch scenario {
case .standard:
rules = [
activeRule(named: "Work Time", hardMode: false, now: now, calendar: calendar),
upcomingRule(named: "Sleep", now: now, calendar: calendar),
]
case .hardModeActive:
rules = [
activeRule(named: "Locked In", hardMode: true, now: now, calendar: calendar),
upcomingRule(named: "Sleep", now: now, calendar: calendar),
]
case .limits:
let timeKeeper = BlockingRule(
name: "Time Keeper",
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 45)),
days: Weekday.everyDay)
let gateKeeper = BlockingRule(
name: "Gate Keeper",
configuration: .openLimit(OpenLimitConfig(maxOpens: 5)),
days: Weekday.everyDay)
let doomScroll = BlockingRule(
name: "Doom Scroll",
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 30)),
days: Weekday.everyDay)
usage?.usageByRule[timeKeeper.id] = RuleUsage(minutesUsed: 18)
usage?.usageByRule[gateKeeper.id] = RuleUsage(opensUsed: 2)
usage?.usageByRule[doomScroll.id] = RuleUsage(minutesUsed: 30)
rules = [timeKeeper, gateKeeper, doomScroll]
}
// Relationships are wired only after both sides are managed
// (see BlockingRule.appList).
for rule in rules {
context.insert(rule)
rule.appList = distractions
}
}
/// A schedule rule whose window started up to an hour ago and runs for
/// several hours, clamped so it never crosses midnight accidentally.
static func activeRule(
named name: String, hardMode: Bool, now: Date, calendar: Calendar = .current
) -> BlockingRule {
let nowMinutes = minutesIntoDay(of: now, calendar: calendar)
let start = max(0, nowMinutes - 60)
let end = min(24 * 60 - 1, nowMinutes + 6 * 60)
return BlockingRule(
name: name,
configuration: .schedule(ScheduleConfig(startMinutes: start, endMinutes: end)),
hardMode: hardMode,
days: Weekday.everyDay
)
}
/// A schedule rule starting a couple of hours from now (possibly wrapping
/// past midnight, which simply makes it start tomorrow).
static func upcomingRule(
named name: String, now: Date, calendar: Calendar = .current
) -> BlockingRule {
let nowMinutes = minutesIntoDay(of: now, calendar: calendar)
let start = (nowMinutes + 2 * 60) % (24 * 60)
let end = (start + 8 * 60) % (24 * 60)
return BlockingRule(
name: name,
configuration: .schedule(ScheduleConfig(startMinutes: start, endMinutes: end)),
days: Weekday.everyDay
)
}
private static func minutesIntoDay(of date: Date, calendar: Calendar) -> Int {
let components = calendar.dateComponents([.hour, .minute], from: date)
return (components.hour ?? 0) * 60 + (components.minute ?? 0)
}
}