@tryterra/react-native 0.0.0-stage → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/TerraReactNative.podspec +48 -0
  4. package/android/build.gradle +87 -0
  5. package/android/src/bridgeCore/kotlin/README.md +1 -0
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
  8. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +468 -0
  9. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
  10. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
  11. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
  12. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
  13. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +258 -0
  14. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +52 -0
  15. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
  16. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
  17. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +493 -0
  18. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +897 -0
  19. package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
  20. package/android/src/main/AndroidManifest.xml +24 -0
  21. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
  22. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
  23. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
  24. package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
  25. package/docs/availability.md +246 -0
  26. package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
  27. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  28. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +81513 -0
  29. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2150 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2150 -0
  31. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2127 -0
  33. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  34. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  36. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  37. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6473 -0
  38. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  39. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +81513 -0
  40. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2150 -0
  41. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2150 -0
  42. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  43. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2127 -0
  44. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +81513 -0
  45. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2150 -0
  46. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2150 -0
  47. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  48. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2127 -0
  49. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  50. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  51. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
  52. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  53. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  54. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6474 -0
  55. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6392 -0
  56. package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
  57. package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
  58. package/ios/TerraBridgeCore/Conversions.swift +467 -0
  59. package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
  60. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +795 -0
  61. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +1317 -0
  62. package/ios/TerraBridgeCore/Locked.swift +15 -0
  63. package/ios/TerraBridgeCore/README.md +1 -0
  64. package/ios/TerraBridgeCore/Rejections.swift +130 -0
  65. package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
  66. package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
  67. package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
  68. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +254 -0
  69. package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
  70. package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
  71. package/ios/TerraBridgeModule.mm +79 -0
  72. package/ios/TerraBridgeModuleImpl.swift +49 -0
  73. package/ios/TerraReactNative.swift +19 -0
  74. package/lib/EndUser.d.ts +12 -0
  75. package/lib/EndUser.js +13 -0
  76. package/lib/NativeTerraBridge.d.ts +23 -0
  77. package/lib/NativeTerraBridge.js +13 -0
  78. package/lib/Terra.d.ts +154 -0
  79. package/lib/Terra.js +280 -0
  80. package/lib/TerraError.d.ts +42 -0
  81. package/lib/TerraError.js +95 -0
  82. package/lib/createTerra.d.ts +8 -0
  83. package/lib/createTerra.js +19 -0
  84. package/lib/generated/client.d.ts +92 -0
  85. package/lib/generated/client.js +196 -0
  86. package/lib/generated/codecs.d.ts +135 -0
  87. package/lib/generated/codecs.js +1358 -0
  88. package/lib/generated/errors.d.ts +13 -0
  89. package/lib/generated/errors.js +68 -0
  90. package/lib/generated/ops.d.ts +13 -0
  91. package/lib/generated/ops.js +137 -0
  92. package/lib/generated/streams.d.ts +23 -0
  93. package/lib/generated/streams.js +58 -0
  94. package/lib/generated/types.d.ts +481 -0
  95. package/lib/generated/types.js +92 -0
  96. package/lib/index.d.ts +29 -0
  97. package/lib/index.js +55 -0
  98. package/package.json +71 -4
  99. package/src/EndUser.ts +15 -0
  100. package/src/NativeTerraBridge.ts +27 -0
  101. package/src/Terra.ts +462 -0
  102. package/src/TerraError.ts +116 -0
  103. package/src/createTerra.ts +18 -0
  104. package/src/generated/client.ts +243 -0
  105. package/src/generated/codecs.ts +1287 -0
  106. package/src/generated/errors.ts +113 -0
  107. package/src/generated/ops.ts +197 -0
  108. package/src/generated/streams.ts +74 -0
  109. package/src/generated/types.ts +667 -0
  110. package/src/index.ts +52 -0
@@ -0,0 +1,89 @@
1
+ import Foundation
2
+ import TerraSDK
3
+
4
+ public enum BridgeBootstrap {
5
+ /// The Info.plist key carrying the developer id.
6
+ public static let devIDKey = "TerraDevID"
7
+
8
+ public struct Configuration: Sendable, Equatable {
9
+ public let devID: String
10
+
11
+ public init(devID: String) {
12
+ self.devID = devID
13
+ }
14
+
15
+ /// The configuration in `bundle`'s Info.plist, or nil when it names no dev id.
16
+ public init?(bundle: Bundle) {
17
+ guard let devID = (bundle.object(forInfoDictionaryKey: BridgeBootstrap.devIDKey) as? String)?
18
+ .trimmingCharacters(in: .whitespacesAndNewlines), !devID.isEmpty
19
+ else { return nil }
20
+ self.init(devID: devID)
21
+ }
22
+ }
23
+
24
+ private static let parked = Locked<TerraBridge?>(nil)
25
+
26
+ /// The bridge bootstrap parked, or nil when it has not run.
27
+ public static var current: TerraBridge? { parked.withLock { $0 } }
28
+
29
+ @discardableResult
30
+ public static func bootstrap(bundle: Bundle = .main) -> TerraBridge? {
31
+ guard let configuration = Configuration(bundle: bundle) else { return nil }
32
+ return bootstrap(configuration)
33
+ }
34
+
35
+ @discardableResult
36
+ public static func bootstrap(_ configuration: Configuration) -> TerraBridge {
37
+ bootstrap(configuration, register: registerBackgroundSync) { provider in
38
+ Terra(devID: configuration.devID, tokenProvider: provider)
39
+ }
40
+ }
41
+
42
+ static func bootstrap(
43
+ _ configuration: Configuration,
44
+ register: (Terra) -> Bool,
45
+ client: (_ tokenProvider: @escaping TerraBridge.TokenProvider) -> Terra
46
+ ) -> TerraBridge {
47
+ parked.withLock { parked in
48
+ if let existing = parked { return existing }
49
+ let host = LaunchHost(devID: configuration.devID)
50
+ let bridge = TerraBridge(host: host, client: client)
51
+ host.registered.withLock { $0 = register(bridge.terra) }
52
+ parked = bridge
53
+ return bridge
54
+ }
55
+ }
56
+
57
+ public static func attach(
58
+ missingLine: String,
59
+ emit: @escaping @Sendable (_ subscriptionId: String, _ message: String) -> Void
60
+ ) throws(BridgeRejection) -> BridgeSession {
61
+ guard let bridge = current else {
62
+ throw BridgeRejection(payload: Rejections.payload(for: BridgeError.notBootstrapped(line: missingLine)))
63
+ }
64
+ return bridge.attach(emit: emit)
65
+ }
66
+
67
+ static func reset() {
68
+ parked.withLock { $0 = nil }
69
+ }
70
+
71
+ private static func registerBackgroundSync(_ terra: Terra) -> Bool {
72
+ #if os(iOS)
73
+ return BackgroundSync.register(terra: terra)
74
+ #else
75
+ return false
76
+ #endif
77
+ }
78
+ }
79
+
80
+ final class LaunchHost: BridgeHost {
81
+ let devID: String?
82
+ let registered = Locked(false)
83
+
84
+ init(devID: String) {
85
+ self.devID = devID
86
+ }
87
+
88
+ func backgroundRegistered() async -> Bool { registered.withLock { $0 } }
89
+ }
@@ -0,0 +1,13 @@
1
+ import Foundation
2
+
3
+ /// The parts of an op that are not the SDK: the bootstrap's own record of what it set up.
4
+ ///
5
+ /// A seam so that everything in front of it runs on a macOS host with no simulator. The production
6
+ /// conformance belongs to bootstrap (P5), which is the code that registered the task.
7
+ public protocol BridgeHost: Sendable {
8
+ /// The dev id the native bootstrap was configured with.
9
+ var devID: String? { get }
10
+
11
+ /// Whether bootstrap registered background sync at launch. False is a host setup bug.
12
+ func backgroundRegistered() async -> Bool
13
+ }
@@ -0,0 +1,467 @@
1
+ import Foundation
2
+ import TerraSDK
3
+
4
+ /// Between the SDK's types and the generated wire DTOs.
5
+ ///
6
+ /// Every `switch` is exhaustive with no `default`, so a case added on either side is a compile
7
+ /// error here rather than a value that maps to something plausible. Unqualified names are the
8
+ /// generated wire types; the SDK's are spelled `TerraSDK.`.
9
+ enum Conversions {
10
+
11
+ // MARK: Input the decoder accepted and the SDK cannot
12
+
13
+ /// A string field that decoded but is not the instant or id it names. Reported as the same
14
+ /// `bridge.invalid_payload` the generated decoder raises, with the path and never the value.
15
+ static func instant(_ text: String, at path: String) throws -> Date {
16
+ guard let date = BridgeInstant.parse(text) else { throw BridgeError.invalidPayload(path: path) }
17
+ return date
18
+ }
19
+
20
+ static func uuid(_ text: String, at path: String) throws -> UUID {
21
+ guard let id = UUID(uuidString: text) else { throw BridgeError.invalidPayload(path: path) }
22
+ return id
23
+ }
24
+
25
+ static func interval(start: String, end: String, at path: String) throws -> DateInterval {
26
+ let from = try instant(start, at: "\(path).start")
27
+ let to = try instant(end, at: "\(path).end")
28
+ guard from <= to else { throw BridgeError.invalidPayload(path: "\(path).end") }
29
+ return DateInterval(start: from, end: to)
30
+ }
31
+
32
+ /// The calendar day an instant falls on in `zone`, as `yyyy-MM-dd`.
33
+ static func localDate(_ date: Date, in zone: TimeZone) -> String {
34
+ var calendar = Calendar(identifier: .gregorian)
35
+ calendar.timeZone = zone
36
+ let parts = calendar.dateComponents([.year, .month, .day], from: date)
37
+ return day(year: parts.year ?? 0, month: parts.month ?? 0, day: parts.day ?? 0)
38
+ }
39
+
40
+ static func localDate(_ components: DateComponents) -> String {
41
+ day(year: components.year ?? 0, month: components.month ?? 0, day: components.day ?? 0)
42
+ }
43
+
44
+ private static func day(year: Int, month: Int, day: Int) -> String {
45
+ String(format: "%04d-%02d-%02d", year, month, day)
46
+ }
47
+
48
+ // MARK: Enums
49
+
50
+ // Swift 6 requires `@unknown default` for TerraSDK's non-frozen binary enums.
51
+
52
+ static func source(_ wire: Source?) throws -> TerraSDK.Source {
53
+ switch wire ?? .health {
54
+ case .health: return .health
55
+ case .samsungHealth: throw BridgeError.unsupportedOnPlatform(op: "samsungHealth")
56
+ }
57
+ }
58
+
59
+ static func source(_ source: TerraSDK.Source) -> Source {
60
+ switch source {
61
+ case .health: return .health
62
+ @unknown default: return .health
63
+ }
64
+ }
65
+
66
+ static func endUser(_ wire: EndUser) -> TerraSDK.EndUser {
67
+ switch wire {
68
+ case .reference(let id): return .reference(id)
69
+ case .anonymous: return .anonymous
70
+ }
71
+ }
72
+
73
+ static func category(_ wire: DataCategory) -> TerraSDK.DataCategory {
74
+ switch wire {
75
+ case .activity: return .activity
76
+ case .body: return .body
77
+ case .daily: return .daily
78
+ case .sleep: return .sleep
79
+ case .nutrition: return .nutrition
80
+ }
81
+ }
82
+
83
+ /// Nil for the categories the surface excludes: there is no wire name to report them under.
84
+ static func category(_ category: TerraSDK.DataCategory) -> DataCategory? {
85
+ switch category {
86
+ case .activity: return .activity
87
+ case .body: return .body
88
+ case .daily: return .daily
89
+ case .sleep: return .sleep
90
+ case .nutrition: return .nutrition
91
+ case .menstruation, .athlete, .healthObservation: return nil
92
+ @unknown default: return nil
93
+ }
94
+ }
95
+
96
+ static func permissionStatus(_ status: TerraSDK.PermissionStatus) -> PermissionStatus {
97
+ switch status {
98
+ case .granted: return .granted
99
+ case .denied: return .denied
100
+ case .notDetermined: return .notDetermined
101
+ case .notDisclosed: return .notDisclosed
102
+ case .unavailable: return .unavailable
103
+ case .unsupported: return .unsupported
104
+ @unknown default: return .unavailable
105
+ }
106
+ }
107
+
108
+ static func motionContext(_ context: TerraSDK.MotionContext) -> MotionContext {
109
+ switch context {
110
+ case .unknown: return .unknown
111
+ case .sedentary: return .sedentary
112
+ case .active: return .active
113
+ @unknown default: return .unknown
114
+ }
115
+ }
116
+
117
+ static func deviceType(_ type: TerraSDK.DeviceType) -> String {
118
+ switch type {
119
+ case .phone: return "phone"
120
+ case .scale: return "scale"
121
+ case .sensor: return "sensor"
122
+ @unknown default: return "unknown"
123
+ }
124
+ }
125
+
126
+ // MARK: Unions
127
+
128
+ static func timeRange(_ wire: TimeRange, at path: String) throws -> TerraSDK.TimeRange {
129
+ switch wire {
130
+ case .lastHours(let hours): return .lastHours(hours)
131
+ case .lastDays(let days): return .lastDays(days)
132
+ case .today: return .today
133
+ case .between(let start, let end):
134
+ let window = try interval(start: start, end: end, at: path)
135
+ return .between(window.start, window.end)
136
+ }
137
+ }
138
+
139
+ static func sleepStage(_ stage: TerraSDK.SleepStage) -> SleepStage {
140
+ switch stage {
141
+ case .inBed: return .inBed
142
+ case .awake: return .awake
143
+ case .asleep: return .asleep
144
+ case .light: return .light
145
+ case .deep: return .deep
146
+ case .rem: return .rem
147
+ case .unknown(let raw): return .unknown(raw: raw)
148
+ @unknown default: return .unknown(raw: String(describing: stage))
149
+ }
150
+ }
151
+
152
+ static func sleepStage(_ wire: SleepStage) -> TerraSDK.SleepStage {
153
+ switch wire {
154
+ case .inBed: return .inBed
155
+ case .awake: return .awake
156
+ case .asleep: return .asleep
157
+ case .light: return .light
158
+ case .deep: return .deep
159
+ case .rem: return .rem
160
+ case .unknown(let raw): return .unknown(raw)
161
+ }
162
+ }
163
+
164
+ static func activity(_ activity: TerraSDK.WorkoutActivity) -> WorkoutActivity {
165
+ switch activity {
166
+ case .running: return .running
167
+ case .walking: return .walking
168
+ case .cycling: return .cycling
169
+ case .swimming: return .swimming
170
+ case .strengthTraining: return .strengthTraining
171
+ case .other(let label): return .other(label: label)
172
+ @unknown default: return .other(label: String(describing: activity))
173
+ }
174
+ }
175
+
176
+ static func activity(_ wire: WorkoutActivity) -> TerraSDK.WorkoutActivity {
177
+ switch wire {
178
+ case .running: return .running
179
+ case .walking: return .walking
180
+ case .cycling: return .cycling
181
+ case .swimming: return .swimming
182
+ case .strengthTraining: return .strengthTraining
183
+ case .other(let label): return .other(label)
184
+ }
185
+ }
186
+
187
+ /// The order a grant is listed in. Android sorts by the same names, so one grant is one byte
188
+ /// sequence on both platforms.
189
+ static func sortKey(_ activity: WorkoutActivity) -> String {
190
+ switch activity {
191
+ case .running: return "Running"
192
+ case .walking: return "Walking"
193
+ case .cycling: return "Cycling"
194
+ case .swimming: return "Swimming"
195
+ case .strengthTraining: return "Strength Training"
196
+ case .other(let label): return label
197
+ }
198
+ }
199
+
200
+ static func diagnosticsState(_ state: TerraSDK.DiagnosticsState) -> DiagnosticsState {
201
+ switch state {
202
+ case .healthy: return .healthy
203
+ case .quiet(let since): return .quiet(since: BridgeInstant.format(since))
204
+ // Apple does not disclose why, so the reason is absent rather than guessed.
205
+ case .backgroundRestricted: return .backgroundRestricted(reason: nil)
206
+ case .backgroundRefreshUnscheduled: return .backgroundRefreshUnscheduled
207
+ case .pendingRetry: return .pendingRetry
208
+ case .linkRevoked: return .linkRevoked
209
+ case .devIDMismatch: return .devIDMismatch
210
+ @unknown default: return .pendingRetry
211
+ }
212
+ }
213
+
214
+ // MARK: Records
215
+
216
+ static func connection(_ connection: TerraSDK.Connection) -> Connection {
217
+ Connection(source: source(connection.source), userId: connection.userID)
218
+ }
219
+
220
+ static func sampleSource(_ source: TerraSDK.SampleSource) -> SampleSource {
221
+ SampleSource(
222
+ name: source.name,
223
+ bundleId: source.bundleID,
224
+ productType: source.productType,
225
+ deviceType: source.deviceType.map(deviceType))
226
+ }
227
+
228
+ static func heartRate(_ sample: TerraSDK.HeartRateSample) -> HeartRateSample {
229
+ HeartRateSample(
230
+ id: sample.id.uuidString,
231
+ time: BridgeInstant.format(sample.time),
232
+ beatsPerMinute: sample.beatsPerMinute,
233
+ source: sampleSource(sample.source),
234
+ motionContext: sample.motionContext.map(motionContext),
235
+ userEntered: sample.userEntered,
236
+ timeZone: sample.timeZone?.identifier)
237
+ }
238
+
239
+ static func steps(_ sample: TerraSDK.StepCount) -> StepCount {
240
+ StepCount(
241
+ id: sample.id,
242
+ start: BridgeInstant.format(sample.interval.start),
243
+ end: BridgeInstant.format(sample.interval.end),
244
+ steps: sample.steps,
245
+ source: sampleSource(sample.source),
246
+ userEntered: sample.userEntered,
247
+ timeZone: sample.timeZone?.identifier)
248
+ }
249
+
250
+ static func segment(_ segment: TerraSDK.SleepSegment) -> SleepSegment {
251
+ SleepSegment(
252
+ id: segment.id.uuidString,
253
+ start: BridgeInstant.format(segment.interval.start),
254
+ end: BridgeInstant.format(segment.interval.end),
255
+ stage: sleepStage(segment.stage),
256
+ source: sampleSource(segment.source),
257
+ userEntered: segment.userEntered,
258
+ timeZone: segment.timeZone?.identifier)
259
+ }
260
+
261
+ static func workout(_ workout: TerraSDK.Workout) -> Workout {
262
+ Workout(
263
+ id: workout.id.uuidString,
264
+ start: BridgeInstant.format(workout.interval.start),
265
+ end: BridgeInstant.format(workout.interval.end),
266
+ activity: activity(workout.activity),
267
+ activityTypeCode: workout.activityTypeCode,
268
+ indoor: workout.indoor,
269
+ activeEnergyKilocalories: workout.activeEnergyKilocalories,
270
+ distanceMeters: workout.distanceMeters,
271
+ source: sampleSource(workout.source),
272
+ userEntered: workout.userEntered,
273
+ timeZone: workout.timeZone?.identifier)
274
+ }
275
+
276
+ /// A day, not an instant: sending the instant would let the receiver re-derive the day in its
277
+ /// own zone and get a different one either side of midnight.
278
+ static func change(_ change: TerraSDK.HealthChange, zone: TimeZone) -> HealthChange? {
279
+ category(change.category).map { HealthChange(category: $0, day: localDate(change.day, in: zone)) }
280
+ }
281
+
282
+ /// `@unknown default` because the binary SDK is built with library evolution, where this enum
283
+ /// can grow; `SchemaParityTests` is what fails when it does. A status this build predates
284
+ /// reads as one it cannot schedule on.
285
+ static func schedulingStatus(_ status: TerraSDK.WorkoutSchedulingStatus) -> WorkoutSchedulingStatus {
286
+ switch status {
287
+ case .unsupported: .unsupported
288
+ case .notDetermined: .notDetermined
289
+ case .denied: .denied
290
+ case .authorized: .authorized
291
+ @unknown default: .unsupported
292
+ }
293
+ }
294
+
295
+ static func plannedWorkout(_ plan: TerraSDK.PlannedWorkout) -> PlannedWorkout {
296
+ PlannedWorkout(
297
+ id: plan.id,
298
+ plannedOn: localDate(plan.plannedOn),
299
+ activity: activity(plan.activity),
300
+ title: plan.title,
301
+ completedAt: plan.completedAt.map(BridgeInstant.format))
302
+ }
303
+
304
+ /// Sorted, because a `Set` has no order and JSON has no set.
305
+ static func access(_ access: TerraSDK.HealthAccess) -> HealthAccess {
306
+ HealthAccess(
307
+ read: access.read.compactMap(category).sorted { $0.rawValue < $1.rawValue },
308
+ write: access.write.compactMap(category).sorted { $0.rawValue < $1.rawValue },
309
+ workouts: ordered(access.workouts
310
+ .map { WorkoutScope(activity: activity($0.activity), energy: $0.energy, distance: $0.distance) }))
311
+ }
312
+
313
+ static func ordered(_ scopes: [WorkoutScope]) -> [WorkoutScope] {
314
+ scopes.sorted { ($0.sortKey, $0.wireKey) < ($1.sortKey, $1.wireKey) }
315
+ }
316
+
317
+ static func scope(_ wire: WorkoutScope) -> TerraSDK.WorkoutScope {
318
+ TerraSDK.WorkoutScope(activity: activity(wire.activity), energy: wire.energy, distance: wire.distance)
319
+ }
320
+
321
+ // MARK: Diagnostics
322
+
323
+ /// The SDK's own `Encodable` spelling, with the bridge's instant format. Not reshaped: a second
324
+ /// encoding here would be a contract no vector pins.
325
+ static func event(_ event: TerraSDK.TerraEvent) throws -> TerraEvent {
326
+ let encoder = JSONEncoder()
327
+ encoder.dateEncodingStrategy = .custom { date, encoder in
328
+ var container = encoder.singleValueContainer()
329
+ try container.encode(BridgeInstant.format(date))
330
+ }
331
+ let json = try BridgeDispatch.makeDecoder().decode(TerraEvent.self, from: encoder.encode(event))
332
+ try TerraEventEnvelope.validate(json)
333
+ return json
334
+ }
335
+
336
+ static func report(_ report: TerraSDK.SupportReport) throws -> SupportReport {
337
+ SupportReport(
338
+ sdkVersion: report.sdkVersion,
339
+ generatedAt: BridgeInstant.format(report.generatedAt),
340
+ state: diagnosticsState(report.state),
341
+ lastDelivery: report.lastDelivery.map(BridgeInstant.format),
342
+ isConnected: report.isConnected,
343
+ requestedCategories: report.requestedCategories,
344
+ requestedWrites: report.requestedWrites,
345
+ requestedWorkouts: report.requestedWorkouts,
346
+ recentEvents: try report.recentEvents.map(event))
347
+ }
348
+
349
+ // MARK: Write entries
350
+
351
+ /// An absent record id is left to the SDK's own default rather than minted here.
352
+ static func entry(_ wire: HeartRateEntry, at path: String) throws -> TerraSDK.HeartRateEntry {
353
+ let time = try instant(wire.time, at: "\(path).time")
354
+ guard let id = wire.recordId else { return .init(time: time, beatsPerMinute: wire.beatsPerMinute) }
355
+ return .init(time: time, beatsPerMinute: wire.beatsPerMinute, recordID: try uuid(id, at: "\(path).recordId"))
356
+ }
357
+
358
+ static func entry(_ wire: StepEntry, at path: String) throws -> TerraSDK.StepEntry {
359
+ let interval = try Self.interval(start: wire.start, end: wire.end, at: path)
360
+ guard let id = wire.recordId else { return .init(interval: interval, steps: wire.steps) }
361
+ return .init(interval: interval, steps: wire.steps, recordID: try uuid(id, at: "\(path).recordId"))
362
+ }
363
+
364
+ static func entry(_ wire: SleepEntry, at path: String) throws -> TerraSDK.SleepEntry {
365
+ let interval = try Self.interval(start: wire.start, end: wire.end, at: path)
366
+ let stage = sleepStage(wire.stage)
367
+ guard let id = wire.recordId else { return .init(interval: interval, stage: stage) }
368
+ return .init(interval: interval, stage: stage, recordID: try uuid(id, at: "\(path).recordId"))
369
+ }
370
+
371
+ static func entry(_ wire: WorkoutEntry, at path: String) throws -> TerraSDK.WorkoutEntry {
372
+ let interval = try Self.interval(start: wire.start, end: wire.end, at: path)
373
+ let activity = Self.activity(wire.activity)
374
+ guard let id = wire.recordId else {
375
+ return .init(
376
+ interval: interval, activity: activity,
377
+ totalEnergyKilocalories: wire.totalEnergyKilocalories,
378
+ totalDistanceMeters: wire.totalDistanceMeters)
379
+ }
380
+ return .init(
381
+ interval: interval, activity: activity,
382
+ totalEnergyKilocalories: wire.totalEnergyKilocalories,
383
+ totalDistanceMeters: wire.totalDistanceMeters,
384
+ recordID: try uuid(id, at: "\(path).recordId"))
385
+ }
386
+
387
+ static func entry(_ wire: BodyEntry, at path: String) throws -> TerraSDK.BodyEntry {
388
+ let time = try instant(wire.time, at: "\(path).time")
389
+ var entry = try wire.recordId.map { TerraSDK.BodyEntry(time: time, recordID: try uuid($0, at: "\(path).recordId")) }
390
+ ?? TerraSDK.BodyEntry(time: time)
391
+ entry.bodyMassKilograms = wire.bodyMassKilograms
392
+ entry.bodyFatPercent = wire.bodyFatPercent
393
+ entry.bodyMassIndex = wire.bodyMassIndex
394
+ entry.heightMeters = wire.heightMeters
395
+ entry.leanBodyMassKilograms = wire.leanBodyMassKilograms
396
+ entry.bodyTemperatureCelsius = wire.bodyTemperatureCelsius
397
+ entry.oxygenSaturationPercent = wire.oxygenSaturationPercent
398
+ entry.bloodGlucoseMilligramsPerDeciliter = wire.bloodGlucoseMilligramsPerDeciliter
399
+ entry.bloodPressureSystolicMillimetersOfMercury = wire.bloodPressureSystolicMillimetersOfMercury
400
+ entry.bloodPressureDiastolicMillimetersOfMercury = wire.bloodPressureDiastolicMillimetersOfMercury
401
+ return entry
402
+ }
403
+
404
+ static func entry(_ wire: NutritionEntry, at path: String) throws -> TerraSDK.NutritionEntry {
405
+ let interval = try Self.interval(start: wire.start, end: wire.end, at: path)
406
+ let nutrients = Self.nutrients(wire.nutrients)
407
+ guard let id = wire.recordId else {
408
+ return .init(interval: interval, name: wire.name, nutrients: nutrients)
409
+ }
410
+ return .init(
411
+ interval: interval, name: wire.name, nutrients: nutrients, recordID: try uuid(id, at: "\(path).recordId"))
412
+ }
413
+
414
+ /// All thirty-nine, by name. The unit is in every name on both sides, so a mismatched pair
415
+ /// does not compile.
416
+ private static func nutrients(_ wire: Nutrients) -> TerraSDK.NutritionEntry.Nutrients {
417
+ var out = TerraSDK.NutritionEntry.Nutrients()
418
+ out.energyKilocalories = wire.energyKilocalories
419
+ out.proteinGrams = wire.proteinGrams
420
+ out.carbohydratesGrams = wire.carbohydratesGrams
421
+ out.fiberGrams = wire.fiberGrams
422
+ out.sugarGrams = wire.sugarGrams
423
+ out.fatTotalGrams = wire.fatTotalGrams
424
+ out.fatSaturatedGrams = wire.fatSaturatedGrams
425
+ out.fatMonounsaturatedGrams = wire.fatMonounsaturatedGrams
426
+ out.fatPolyunsaturatedGrams = wire.fatPolyunsaturatedGrams
427
+ out.cholesterolMilligrams = wire.cholesterolMilligrams
428
+ out.sodiumMilligrams = wire.sodiumMilligrams
429
+ out.potassiumMilligrams = wire.potassiumMilligrams
430
+ out.calciumMilligrams = wire.calciumMilligrams
431
+ out.ironMilligrams = wire.ironMilligrams
432
+ out.magnesiumMilligrams = wire.magnesiumMilligrams
433
+ out.zincMilligrams = wire.zincMilligrams
434
+ out.waterLiters = wire.waterLiters
435
+ out.caffeineMilligrams = wire.caffeineMilligrams
436
+ out.vitaminAMicrograms = wire.vitaminAMicrograms
437
+ out.vitaminCMilligrams = wire.vitaminCMilligrams
438
+ out.vitaminDMicrograms = wire.vitaminDMicrograms
439
+ out.vitaminEMilligrams = wire.vitaminEMilligrams
440
+ out.vitaminKMicrograms = wire.vitaminKMicrograms
441
+ out.vitaminB6Milligrams = wire.vitaminB6Milligrams
442
+ out.vitaminB12Micrograms = wire.vitaminB12Micrograms
443
+ out.folateMicrograms = wire.folateMicrograms
444
+ out.thiaminMilligrams = wire.thiaminMilligrams
445
+ out.riboflavinMilligrams = wire.riboflavinMilligrams
446
+ out.niacinMilligrams = wire.niacinMilligrams
447
+ out.biotinMicrograms = wire.biotinMicrograms
448
+ out.pantothenicAcidMilligrams = wire.pantothenicAcidMilligrams
449
+ out.phosphorusMilligrams = wire.phosphorusMilligrams
450
+ out.iodineMicrograms = wire.iodineMicrograms
451
+ out.chromiumMicrograms = wire.chromiumMicrograms
452
+ out.copperMilligrams = wire.copperMilligrams
453
+ out.manganeseMilligrams = wire.manganeseMilligrams
454
+ out.molybdenumMicrograms = wire.molybdenumMicrograms
455
+ out.seleniumMicrograms = wire.seleniumMicrograms
456
+ out.chlorideMilligrams = wire.chlorideMilligrams
457
+ return out
458
+ }
459
+ }
460
+
461
+ private extension WorkoutScope {
462
+ var sortKey: String { Conversions.sortKey(activity) }
463
+
464
+ var wireKey: String {
465
+ (try? BridgeDispatch.makeEncoder().encode(self)).map { String(decoding: $0, as: UTF8.self) } ?? ""
466
+ }
467
+ }