@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.
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/TerraReactNative.podspec +48 -0
- package/android/build.gradle +87 -0
- package/android/src/bridgeCore/kotlin/README.md +1 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +468 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +258 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +52 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +493 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +897 -0
- package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
- package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
- package/docs/availability.md +246 -0
- package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6473 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6474 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6392 -0
- package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
- package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
- package/ios/TerraBridgeCore/Conversions.swift +467 -0
- package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
- package/ios/TerraBridgeCore/Generated/BridgeOps.swift +795 -0
- package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +1317 -0
- package/ios/TerraBridgeCore/Locked.swift +15 -0
- package/ios/TerraBridgeCore/README.md +1 -0
- package/ios/TerraBridgeCore/Rejections.swift +130 -0
- package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
- package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
- package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
- package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +254 -0
- package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
- package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
- package/ios/TerraBridgeModule.mm +79 -0
- package/ios/TerraBridgeModuleImpl.swift +49 -0
- package/ios/TerraReactNative.swift +19 -0
- package/lib/EndUser.d.ts +12 -0
- package/lib/EndUser.js +13 -0
- package/lib/NativeTerraBridge.d.ts +23 -0
- package/lib/NativeTerraBridge.js +13 -0
- package/lib/Terra.d.ts +154 -0
- package/lib/Terra.js +280 -0
- package/lib/TerraError.d.ts +42 -0
- package/lib/TerraError.js +95 -0
- package/lib/createTerra.d.ts +8 -0
- package/lib/createTerra.js +19 -0
- package/lib/generated/client.d.ts +92 -0
- package/lib/generated/client.js +196 -0
- package/lib/generated/codecs.d.ts +135 -0
- package/lib/generated/codecs.js +1358 -0
- package/lib/generated/errors.d.ts +13 -0
- package/lib/generated/errors.js +68 -0
- package/lib/generated/ops.d.ts +13 -0
- package/lib/generated/ops.js +137 -0
- package/lib/generated/streams.d.ts +23 -0
- package/lib/generated/streams.js +58 -0
- package/lib/generated/types.d.ts +481 -0
- package/lib/generated/types.js +92 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +55 -0
- package/package.json +71 -4
- package/src/EndUser.ts +15 -0
- package/src/NativeTerraBridge.ts +27 -0
- package/src/Terra.ts +462 -0
- package/src/TerraError.ts +116 -0
- package/src/createTerra.ts +18 -0
- package/src/generated/client.ts +243 -0
- package/src/generated/codecs.ts +1287 -0
- package/src/generated/errors.ts +113 -0
- package/src/generated/ops.ts +197 -0
- package/src/generated/streams.ts +74 -0
- package/src/generated/types.ts +667 -0
- 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
|
+
}
|