TemporaryScheduleOverride.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. //
  2. // TemporaryScheduleOverride.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 1/1/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. public struct TemporaryScheduleOverride: Hashable {
  11. public enum Context: Hashable {
  12. case preMeal
  13. case legacyWorkout
  14. case preset(TemporaryScheduleOverridePreset)
  15. case custom
  16. }
  17. public enum EnactTrigger: Hashable {
  18. case local
  19. case remote(String)
  20. }
  21. public enum Duration: Hashable, Comparable {
  22. case finite(TimeInterval)
  23. case indefinite
  24. public var timeInterval: TimeInterval {
  25. switch self {
  26. case .finite(let interval):
  27. return interval
  28. case .indefinite:
  29. return .infinity
  30. }
  31. }
  32. public var isFinite: Bool {
  33. return timeInterval.isFinite
  34. }
  35. public var isInfinite: Bool {
  36. return timeInterval.isInfinite
  37. }
  38. public static func < (lhs: Duration, rhs: Duration) -> Bool {
  39. return lhs.timeInterval < rhs.timeInterval
  40. }
  41. }
  42. public var context: Context
  43. public var settings: TemporaryScheduleOverrideSettings
  44. public var startDate: Date
  45. public let enactTrigger: EnactTrigger
  46. public let syncIdentifier: UUID
  47. public var actualEnd: End = .natural
  48. public var actualEndDate: Date {
  49. switch actualEnd {
  50. case .natural:
  51. return scheduledEndDate
  52. case .early(let endDate):
  53. return endDate
  54. case .deleted:
  55. return scheduledEndDate
  56. }
  57. }
  58. public var duration: Duration {
  59. didSet {
  60. precondition(duration.timeInterval > 0)
  61. }
  62. }
  63. public var scheduledEndDate: Date {
  64. get {
  65. return startDate + duration.timeInterval
  66. }
  67. set {
  68. precondition(newValue > startDate)
  69. if newValue == .distantFuture {
  70. duration = .indefinite
  71. } else {
  72. duration = .finite(newValue.timeIntervalSince(startDate))
  73. }
  74. }
  75. }
  76. public var activeInterval: DateInterval {
  77. return DateInterval(start: startDate, end: actualEndDate)
  78. }
  79. public var scheduledInterval: DateInterval {
  80. get {
  81. return DateInterval(start: startDate, end: scheduledEndDate)
  82. }
  83. set {
  84. startDate = newValue.start
  85. scheduledEndDate = newValue.end
  86. }
  87. }
  88. public func hasFinished(relativeTo date: Date = Date()) -> Bool {
  89. return date > actualEndDate
  90. }
  91. public init(context: Context, settings: TemporaryScheduleOverrideSettings, startDate: Date, duration: Duration, enactTrigger: EnactTrigger, syncIdentifier: UUID) {
  92. precondition(duration.timeInterval > 0)
  93. self.context = context
  94. self.settings = settings
  95. self.startDate = startDate
  96. self.duration = duration
  97. self.enactTrigger = enactTrigger
  98. self.syncIdentifier = syncIdentifier
  99. }
  100. public func isActive(at date: Date = Date()) -> Bool {
  101. return activeInterval.contains(date)
  102. }
  103. }
  104. extension TemporaryScheduleOverride: RawRepresentable {
  105. public typealias RawValue = [String: Any]
  106. public init?(rawValue: RawValue) {
  107. guard
  108. let contextRawValue = rawValue["context"] as? Context.RawValue,
  109. let context = Context(rawValue: contextRawValue),
  110. let settingsRawValue = rawValue["settings"] as? TemporaryScheduleOverrideSettings.RawValue,
  111. let settings = TemporaryScheduleOverrideSettings(rawValue: settingsRawValue),
  112. let startDateSeconds = rawValue["startDate"] as? TimeInterval,
  113. let durationRawValue = rawValue["duration"] as? Duration.RawValue,
  114. let duration = Duration(rawValue: durationRawValue)
  115. else {
  116. return nil
  117. }
  118. let startDate = Date(timeIntervalSince1970: startDateSeconds)
  119. let enactTrigger: EnactTrigger
  120. if let enactTriggerRaw = rawValue["enactTrigger"] as? EnactTrigger.RawValue,
  121. let storedEnactTrigger = EnactTrigger(rawValue: enactTriggerRaw)
  122. {
  123. enactTrigger = storedEnactTrigger
  124. } else {
  125. enactTrigger = .local
  126. }
  127. let syncIdentifier: UUID
  128. if let syncIdentifierRaw = rawValue["syncIdentifier"] as? String,
  129. let storedSyncIdentifier = UUID(uuidString: syncIdentifierRaw) {
  130. syncIdentifier = storedSyncIdentifier
  131. } else {
  132. syncIdentifier = UUID()
  133. }
  134. self.init(context: context, settings: settings, startDate: startDate, duration: duration, enactTrigger: enactTrigger, syncIdentifier: syncIdentifier)
  135. }
  136. public var rawValue: RawValue {
  137. return [
  138. "context": context.rawValue,
  139. "settings": settings.rawValue,
  140. "startDate": startDate.timeIntervalSince1970,
  141. "duration": duration.rawValue,
  142. "syncIdentifier": syncIdentifier.uuidString,
  143. "enactTrigger": enactTrigger.rawValue,
  144. ]
  145. }
  146. }
  147. extension TemporaryScheduleOverride: Codable {}
  148. extension TemporaryScheduleOverride.Context: RawRepresentable {
  149. public typealias RawValue = [String: Any]
  150. public init?(rawValue: RawValue) {
  151. guard let context = rawValue["context"] as? String else {
  152. return nil
  153. }
  154. switch context {
  155. case "premeal":
  156. self = .preMeal
  157. case "legacyWorkout":
  158. self = .legacyWorkout
  159. case "preset":
  160. guard
  161. let presetRawValue = rawValue["preset"] as? TemporaryScheduleOverridePreset.RawValue,
  162. let preset = TemporaryScheduleOverridePreset(rawValue: presetRawValue)
  163. else {
  164. return nil
  165. }
  166. self = .preset(preset)
  167. case "custom":
  168. self = .custom
  169. default:
  170. return nil
  171. }
  172. }
  173. public var rawValue: RawValue {
  174. switch self {
  175. case .preMeal:
  176. return ["context": "premeal"]
  177. case .legacyWorkout:
  178. return ["context": "legacyWorkout"]
  179. case .preset(let preset):
  180. return [
  181. "context": "preset",
  182. "preset": preset.rawValue
  183. ]
  184. case .custom:
  185. return ["context": "custom"]
  186. }
  187. }
  188. }
  189. extension TemporaryScheduleOverride.Context: Codable {
  190. public init(from decoder: Decoder) throws {
  191. if let string = try? decoder.singleValueContainer().decode(String.self) {
  192. switch string {
  193. case CodableKeys.preMeal.rawValue:
  194. self = .preMeal
  195. case CodableKeys.legacyWorkout.rawValue:
  196. self = .legacyWorkout
  197. case CodableKeys.custom.rawValue:
  198. self = .custom
  199. default:
  200. throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "invalid enumeration"))
  201. }
  202. } else {
  203. let container = try decoder.container(keyedBy: CodableKeys.self)
  204. if let preset = try container.decodeIfPresent(Preset.self, forKey: .preset) {
  205. self = .preset(preset.preset)
  206. } else {
  207. throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "invalid enumeration"))
  208. }
  209. }
  210. }
  211. public func encode(to encoder: Encoder) throws {
  212. switch self {
  213. case .preMeal:
  214. var container = encoder.singleValueContainer()
  215. try container.encode(CodableKeys.preMeal.rawValue)
  216. case .legacyWorkout:
  217. var container = encoder.singleValueContainer()
  218. try container.encode(CodableKeys.legacyWorkout.rawValue)
  219. case .preset(let preset):
  220. var container = encoder.container(keyedBy: CodableKeys.self)
  221. try container.encode(Preset(preset: preset), forKey: .preset)
  222. case .custom:
  223. var container = encoder.singleValueContainer()
  224. try container.encode(CodableKeys.custom.rawValue)
  225. }
  226. }
  227. private struct Preset: Codable {
  228. let preset: TemporaryScheduleOverridePreset
  229. }
  230. private enum CodableKeys: String, CodingKey {
  231. case preMeal
  232. case legacyWorkout
  233. case preset
  234. case custom
  235. }
  236. }
  237. extension TemporaryScheduleOverride.Duration: RawRepresentable {
  238. public typealias RawValue = [String: Any]
  239. public init?(rawValue: RawValue) {
  240. guard let duration = rawValue["duration"] as? String else {
  241. return nil
  242. }
  243. switch duration {
  244. case "finite":
  245. guard let interval = rawValue["interval"] as? TimeInterval else {
  246. return nil
  247. }
  248. self = .finite(interval)
  249. case "indefinite":
  250. self = .indefinite
  251. default:
  252. return nil
  253. }
  254. }
  255. public var rawValue: RawValue {
  256. switch self {
  257. case .finite(let interval):
  258. return [
  259. "duration": "finite",
  260. "interval": interval
  261. ]
  262. case .indefinite:
  263. return ["duration": "indefinite"]
  264. }
  265. }
  266. }
  267. extension TemporaryScheduleOverride.Duration: Codable {
  268. public init(from decoder: Decoder) throws {
  269. if let string = try? decoder.singleValueContainer().decode(String.self) {
  270. switch string {
  271. case CodableKeys.indefinite.rawValue:
  272. self = .indefinite
  273. default:
  274. throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "invalid enumeration"))
  275. }
  276. } else {
  277. let container = try decoder.container(keyedBy: CodableKeys.self)
  278. if let finite = try container.decodeIfPresent(Finite.self, forKey: .finite) {
  279. self = .finite(finite.duration)
  280. } else {
  281. throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "invalid enumeration"))
  282. }
  283. }
  284. }
  285. public func encode(to encoder: Encoder) throws {
  286. switch self {
  287. case .finite(let duration):
  288. var container = encoder.container(keyedBy: CodableKeys.self)
  289. try container.encode(Finite(duration: duration), forKey: .finite)
  290. case .indefinite:
  291. var container = encoder.singleValueContainer()
  292. try container.encode(CodableKeys.indefinite.rawValue)
  293. }
  294. }
  295. private struct Finite: Codable {
  296. let duration: TimeInterval
  297. }
  298. private enum CodableKeys: String, CodingKey {
  299. case finite
  300. case indefinite
  301. }
  302. }
  303. extension TemporaryScheduleOverride.EnactTrigger: RawRepresentable {
  304. public typealias RawValue = [String: Any]
  305. public init?(rawValue: RawValue) {
  306. guard let trigger = rawValue["trigger"] as? String else {
  307. return nil
  308. }
  309. switch trigger {
  310. case "local":
  311. self = .local
  312. case "remote":
  313. guard let remoteAddress = rawValue["remoteAddress"] as? String else {
  314. return nil
  315. }
  316. self = .remote(remoteAddress)
  317. default:
  318. return nil
  319. }
  320. }
  321. public var rawValue: RawValue {
  322. switch self {
  323. case .local:
  324. return ["trigger": "local"]
  325. case .remote(let remoteAddress):
  326. return [
  327. "trigger": "remote",
  328. "remoteAddress": remoteAddress
  329. ]
  330. }
  331. }
  332. }
  333. extension TemporaryScheduleOverride.EnactTrigger: Codable {
  334. public init(from decoder: Decoder) throws {
  335. if let string = try? decoder.singleValueContainer().decode(String.self) {
  336. switch string {
  337. case CodableKeys.local.rawValue:
  338. self = .local
  339. default:
  340. throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "invalid enumeration"))
  341. }
  342. } else {
  343. let container = try decoder.container(keyedBy: CodableKeys.self)
  344. if let remote = try container.decodeIfPresent(Remote.self, forKey: .remote) {
  345. self = .remote(remote.address)
  346. } else {
  347. throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "invalid enumeration"))
  348. }
  349. }
  350. }
  351. public func encode(to encoder: Encoder) throws {
  352. switch self {
  353. case .local:
  354. var container = encoder.singleValueContainer()
  355. try container.encode(CodableKeys.local.rawValue)
  356. case .remote(let address):
  357. var container = encoder.container(keyedBy: CodableKeys.self)
  358. try container.encode(Remote(address: address), forKey: .remote)
  359. }
  360. }
  361. private struct Remote: Codable {
  362. let address: String
  363. }
  364. private enum CodableKeys: String, CodingKey {
  365. case local
  366. case remote
  367. }
  368. }