diff --git a/README.md b/README.md index dd6c3b2..d6065ad 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,83 @@ # TidepoolKotlinAPI -A Kotlin library for connecting to the [Tidepool API][api-docs]. -This library is currently unfinished, so not all requests and schemas are implemented. +A Kotlin library for interacting with the Tidepool API. + +## Features + +- **Dependency Injection**: Uses Koin for clean dependency management +- **Environment Support**: Easy switching between Production, Integration, Dev, and QA environments +- **Kotlinx Serialization**: Type-safe JSON handling with polymorphic serialization +- **Coroutines**: Full support for Kotlin coroutines and suspend functions +- **Repository Pattern**: Clean separation between API and business logic +- **Token Management**: Token-based authentication with flexible token provider + +## Quick Start + +```kotlin +val sdk = TidepoolSDK( + environment = Environments.Production, + getToken = { + // Provide your token retrieval logic + "your-session-token" + } +) + +// Get current user information +val userResult = sdk.getCurrentUserInfo() +userResult.fold( + onSuccess = { user -> println("User: ${user.username}") }, + onFailure = { error -> println("Error: ${error.message}") } +) + +// Get diabetes data for a user +val dataResult = sdk.getUserData( + userId = "user-id", + types = listOf(BaseData.DataType.Cbg, BaseData.DataType.Bolus), + startDate = Instant.now().minus(Duration.ofDays(7)), + endDate = Instant.now() +) + +// Get care partner invitations +val invitationsResult = sdk.getReceivedInvitations() + +// Get trust relationships (care partners) +val trustUsersResult = sdk.getTrustUsers() + +// Clean up when done +sdk.shutdown() +``` + +## Architecture + +This library is structured in multiple modules: + +- **`lib`** - Main SDK module with public API and domain models +- **`data`** - Data layer with repository implementations, DTOs, and Retrofit API interfaces (internal) + +The SDK follows Clean Architecture principles with clear separation between domain models (in `lib`) and data transfer +objects (in `data`). + +## Supported Data Types + +The SDK currently supports the following Tidepool data types: + +- Continuous Glucose (CGM/CBG) +- Bolus insulin doses +- Basal insulin (automated) +- Food entries +- Dosing decisions +- Insulin data + +## Environment Configuration + +Available environments: + +- `Environments.Production` - Production Tidepool environment +- `Environments.Integration` - Integration testing environment +- `Environments.Dev1` - Development environment +- `Environments.Qa1` - QA environment 1 +- `Environments.Qa2` - QA environment 2 ## Example @@ -16,3 +91,4 @@ See the [example application][example]. ## Other languages * [Swift (TidepoolKit)][swift-api] + diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..d4fa49b --- /dev/null +++ b/data/README.md @@ -0,0 +1,226 @@ +# Tidepool Kotlin API - Data Module + +This module contains the data layer implementation for the Tidepool Kotlin API, following Clean +Architecture principles with modern Android development practices. + +## Structure + +``` +data/ +├── src/main/kotlin/org/tidepool/sdk/ +│ ├── api/ # Retrofit API interfaces +│ │ ├── AuthApi.kt # Authentication endpoints +│ │ ├── ConfirmationApi.kt # Email/phone confirmation endpoints +│ │ ├── DataApi.kt # Diabetes data endpoints +│ │ ├── MetadataApi.kt # Metadata endpoints +│ │ └── UserApi.kt # User management endpoints +│ ├── dto/ # Data Transfer Objects +│ │ ├── auth/ # Authentication DTOs +│ │ ├── confirmation/ # Email/phone confirmation DTOs +│ │ ├── data/ # Diabetes data DTOs (bolus, CGM, food, etc.) +│ │ ├── metadata/ # Metadata DTOs +│ │ └── user/ # User management DTOs +│ ├── repository/ # Repository interfaces & implementations +│ │ ├── AuthRepository.kt +│ │ ├── ConfirmationRepository.kt +│ │ ├── DataRepository.kt +│ │ ├── MetadataRepository.kt +│ │ ├── UserRepository.kt +│ │ └── impl/ # Repository implementations +│ ├── deserialization/ # JSON serialization utilities +│ ├── di/ # Dependency injection modules +│ │ └── DataModule.kt # Koin module configuration +│ └── datasource/ # Data source implementations +``` + +## Key Features + +- **Clean Architecture**: Separation of concerns with repository pattern +- **Dependency Injection**: Koin-based DI for easy testing and modularity +- **Type Safety**: Comprehensive DTO classes with Kotlin serialization +- **Async Operations**: Coroutines-based async/await pattern +- **Error Handling**: Result types for safe error handling +- **Polymorphic Serialization**: Support for different diabetes data types + +## Dependencies + +This module leverages modern Android/Kotlin libraries: + +- **Retrofit 3.0.0** - HTTP networking with coroutines support +- **Kotlinx Serialization** - JSON parsing with polymorphic support +- **Koin 4.1.0** - Dependency injection +- **OkHttp 5.1.0** - HTTP client with logging interceptor +- **Coroutines 1.10.2** - Async operations + +## Setup + +### 1. Initialize Dependencies + +```kotlin +// Initialize Koin DI +startKoin { + modules(dataModule) +} + +// Provide environment configuration +val environment = EnvironmentInternal( + url = "https://api.tidepool.org", + auth = AuthenticationServerInternal("https://auth.tidepool.org") +) +``` + +### 2. Inject Repositories + +```kotlin +// Get repositories from Koin +val authRepository: AuthRepository by inject() +val dataRepository: DataRepository by inject() +val userRepository: UserRepository by inject() +val confirmationRepository: ConfirmationRepository by inject() +val metadataRepository: MetadataRepository by inject() +``` + +## Usage Examples + +### Authentication Flow + +```kotlin +// Password-based authentication +val tokenRequest = TokenRequestDto.createWithPassword( + client_id = "your-client-id", + username = "user@example.com", + password = "password" +) + +val tokenResult = authRepository.obtainToken( + realm = RealmDto.Tidepool, + tokenRequest = tokenRequest +) + +tokenResult.fold( + onSuccess = { tokenResponse -> + val sessionToken = tokenResponse.access_token + println("Authenticated successfully") + }, + onFailure = { error -> + println("Authentication failed: ${error.message}") + } +) +``` + +### Email Confirmation + +```kotlin +// Send confirmation email +val confirmationResult = confirmationRepository.sendConfirmation( + sessionToken = sessionToken, + type = ConfirmationType.SIGNUP, + emailAddress = "user@example.com" +) + +// Confirm email with verification code +val confirmResult = confirmationRepository.confirmSignup( + confirmationKey = "verification-key-from-email" +) +``` + +### Fetching Diabetes Data + +```kotlin +// Fetch specific data types for a date range +val dataResult = dataRepository.getDataForUser( + sessionToken = sessionToken, + userId = "user-id", + types = CommaSeparatedArray( + BaseDataDto.DataTypeDto.Bolus, + BaseDataDto.DataTypeDto.Cbg, + BaseDataDto.DataTypeDto.Food + ), + startDate = Instant.now().minus(Duration.ofDays(7)), + endDate = Instant.now() +) + +dataResult.fold( + onSuccess = { dataList -> + // Process different data types polymorphically + dataList.forEach { baseData -> + when (baseData) { + is BolusDataDto -> println("Insulin bolus: ${baseData.normal}u") + is ContinuousGlucoseDataDto -> println("BG: ${baseData.value} mg/dL") + is FoodDataDto -> println("Carbs: ${baseData.nutrition?.carbohydrate?.net}g") + // Handle other data types... + } + } + }, + onFailure = { error -> + println("Failed to fetch data: ${error.message}") + } +) +``` + +### User Management + +```kotlin +// Get user profile +val userResult = userRepository.getUser( + sessionToken = sessionToken, + userId = userId +) + +// Get user metadata +val metadataResult = metadataRepository.getMetadataForUser( + sessionToken = sessionToken, + userId = userId +) +``` + +## Data Types Supported + +The module supports comprehensive diabetes data types: + +- **BolusDataDto** - Insulin bolus data +- **BasalAutomatedDataDto** - Automated basal insulin +- **ContinuousGlucoseDataDto** - CGM readings +- **FoodDataDto** - Food and carbohydrate entries +- **DosingDecisionDataDto** - Automated insulin dosing decisions +- **InsulinDataDto** - General insulin data + +## Error Handling + +All operations return `Result` types for explicit error handling: + +```kotlin +when (val result = repository.someOperation()) { + is Result.Success -> { + val data = result.getOrNull() + // Handle success + } + is Result.Failure -> { + val error = result.exceptionOrNull() + // Handle error + } +} +``` + +## Testing + +The module includes comprehensive test coverage with: + +- **JUnit 5** for unit tests +- **Coroutines Test** for async testing +- **MockWebServer** for API mocking +- **Koin Test** for DI testing + +Run tests with: + +```bash +./gradlew :data:test +``` + +## Architecture Benefits + +- **Testability**: Dependency injection enables easy mocking +- **Maintainability**: Clear separation of concerns +- **Scalability**: Repository pattern supports multiple data sources +- **Type Safety**: Kotlin's type system prevents runtime errors +- **Modern**: Uses latest Android/Kotlin best practices \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts new file mode 100644 index 0000000..d61409c --- /dev/null +++ b/data/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + kotlin("jvm") version "2.2.0" + kotlin("plugin.serialization") version "2.2.0" + id("com.google.devtools.ksp") version "2.2.20-2.0.3" + // Apply the java-library plugin for API and implementation separation. + `java-library` +} + +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } +} + +repositories { + mavenCentral() + google() +} + +dependencies { + // Networking + implementation("com.squareup.retrofit2:retrofit:3.0.0") + implementation("com.squareup.okhttp3:okhttp:5.1.0") + + // Serialization + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + + // Room KMP dependencies for local storage + implementation("androidx.room:room-runtime:2.8.1") + implementation("androidx.sqlite:sqlite-bundled:2.5.0") + implementation("com.squareup.okhttp3:logging-interceptor:5.1.0") + add("ksp", "androidx.room:room-compiler:2.8.1") + + // Koin dependency injection + implementation("io.insert-koin:koin-core:4.1.0") + + // Testing + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") + testImplementation("com.squareup.okhttp3:mockwebserver:5.1.0") + testImplementation("io.insert-koin:koin-test:4.1.0") + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +tasks.named("test") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/AuthenticationServerInternal.kt b/data/src/main/kotlin/org/tidepool/sdk/AuthenticationServerInternal.kt new file mode 100644 index 0000000..9bb2d39 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/AuthenticationServerInternal.kt @@ -0,0 +1,8 @@ +package org.tidepool.sdk + +enum class AuthenticationServerInternal(val url: String){ + Development("https://auth.dev.tidepool.org"), + QA("https://auth.qa2.tidepool.org"), + External("https://auth.external.tidepool.org"), + Production("https://auth.tidepool.org"); +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/EnvironmentInternal.kt b/data/src/main/kotlin/org/tidepool/sdk/EnvironmentInternal.kt new file mode 100644 index 0000000..28bd2f7 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/EnvironmentInternal.kt @@ -0,0 +1,18 @@ +package org.tidepool.sdk + +enum class EnvironmentInternal( + val url: String, + val envCode: String, + val auth: AuthenticationServerInternal +) { + + Production("https://api.tidepool.org", "tidepool", AuthenticationServerInternal.Production), + Integration( + "https://external.integration.tidepool.org", + "integration", + AuthenticationServerInternal.External + ), + Dev1("https://dev1.dev.tidepool.org", "dev", AuthenticationServerInternal.Development), + Qa1("https://qa1.development.tidepool.org", "qa1", AuthenticationServerInternal.QA), + Qa2("https://qa2.development.tidepool.org", "qa2", AuthenticationServerInternal.QA); +} diff --git a/data/src/main/kotlin/org/tidepool/sdk/Example.kt b/data/src/main/kotlin/org/tidepool/sdk/Example.kt new file mode 100644 index 0000000..c29d123 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/Example.kt @@ -0,0 +1,104 @@ +package org.tidepool.sdk + +import kotlinx.coroutines.runBlocking +import org.tidepool.sdk.dto.auth.RealmDto +import org.tidepool.sdk.dto.auth.TokenRequestDto +import java.time.Instant +import kotlin.time.Duration.Companion.days + +/** + * Example usage of the Tidepool Data Module + * + * This demonstrates the typical flow: + * 1. Initialize NetworkModule + * 2. Authenticate with Tidepool + * 3. Fetch user data + * 4. Fetch diabetes data + */ +//fun main() = runBlocking { +// // Initialize the data module +// val networkModule = NetworkModule( +// baseUrl = "https://api.tidepool.org", +// authUrl = "https://auth.tidepool.org" +// ) +// +// // Step 1: Authentication +// println("🔐 Authenticating with Tidepool...") +// +// val tokenRequest = TokenRequestDto.createWithPassword( +// client_id = "your-client-id", +// username = "user@example.com", +// password = "your-password" +// ) +// +// val authResult = networkModule.authRepository.obtainToken( +// realm = RealmDto.Tidepool, +// tokenRequest = tokenRequest +// ) +// +// val sessionToken = authResult.fold( +// onSuccess = { tokenResponse -> +// println("✅ Authentication successful!") +// println(" Access token: ${tokenResponse.access_token.take(20)}...") +// println(" Expires in: ${tokenResponse.expires_in} seconds") +// tokenResponse.access_token +// }, +// onFailure = { error -> +// println("❌ Authentication failed: ${error.message}") +// return@runBlocking +// } +// ) +// +// // Step 2: Get user information +// println("\n👤 Fetching user information...") +// +// val userResult = networkModule.userRepository.getUser( +// sessionToken = sessionToken, +// userId = "user-id-here" +// ) +// +// userResult.fold( +// onSuccess = { user -> +// println("✅ User fetched successfully!") +// println(" Username: ${user.username}") +// println(" Email verified: ${user.emailVerified}") +// }, +// onFailure = { error -> +// println("❌ Failed to fetch user: ${error.message}") +// } +// ) +// +// // Step 3: Fetch diabetes data +// println("\n📊 Fetching diabetes data...") +// +// val dataResult = networkModule.dataRepository.getDataForUser( +// sessionToken = sessionToken, +// userId = "user-id-here", +// types = arrayOf( +// DataTypeDto.Bolus, // Insulin boluses +// DataTypeDto.Cbg, // Continuous glucose readings +// DataTypeDto.Food // Food entries +// ), +// startDate = Instant.now().minusSeconds(7.days.inWholeSeconds), +// endDate = Instant.now(), +// latest = false +// ) +// +// dataResult.fold( +// onSuccess = { dataArray -> +// println("✅ Data fetched successfully!") +// println(" Total data points: ${dataArray.size}") +// +// // Group by type +// val groupedData = dataArray.groupBy { it.type } +// groupedData.forEach { (type, data) -> +// println(" - $type: ${data.size} entries") +// } +// }, +// onFailure = { error -> +// println("❌ Failed to fetch data: ${error.message}") +// } +// ) +// +// println("\n🎉 Example completed!") +//} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/RemoteExt.kt b/data/src/main/kotlin/org/tidepool/sdk/RemoteExt.kt new file mode 100644 index 0000000..58714b5 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/RemoteExt.kt @@ -0,0 +1,9 @@ +package org.tidepool.sdk + +import retrofit2.HttpException + +inline fun runCatchingNetworkExceptions(block: () -> T): Result = try { + Result.success(block()) +} catch (ex: HttpException) { + Result.failure(ex) +} diff --git a/data/src/main/kotlin/org/tidepool/sdk/Session.kt b/data/src/main/kotlin/org/tidepool/sdk/Session.kt new file mode 100644 index 0000000..1d09711 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/Session.kt @@ -0,0 +1,7 @@ +package org.tidepool.sdk + +data class Session( + val sessionToken: String, + val refreshToken: String, + val userId: String, +) \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/api/AuthApi.kt b/data/src/main/kotlin/org/tidepool/sdk/api/AuthApi.kt new file mode 100644 index 0000000..6a16c53 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/api/AuthApi.kt @@ -0,0 +1,48 @@ +package org.tidepool.sdk.api + +import org.tidepool.sdk.dto.auth.TokenRequestDto +import org.tidepool.sdk.dto.auth.TokenResponseDto +import org.tidepool.sdk.dto.auth.RealmDto +import kotlinx.serialization.SerialName +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query +import java.net.URI + +interface AuthApi { + + @POST("/realms/{realm}/protocol/openid-connect/token") + suspend fun obtainToken( + @Path("realm") realm: RealmDto, + @Body grantType: TokenRequestDto, + ): TokenResponseDto + + @GET("/realms/{realm}/protocol/openid-connect/auth?response_type=code") + suspend fun authorize( + @Path("realm") realm: RealmDto, + @Query("client_id") clientId: String, + @Query("scope") scope: Array, + @Query("redirect_uri") redirectUri: URI, + @Query("login_hint") loginHint: String? = null, + @Query("kc_idp_hint") kcIdpHint: String? = null, + @Query("prompt") prompt: PromptType? = null, + ) + + enum class PromptType { + @SerialName("none") + None, + + @SerialName("login") + Login + } + + enum class ScopeType { + @SerialName("openid") + Openid, + + @SerialName("email") + Email + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/api/ConfirmationApi.kt b/data/src/main/kotlin/org/tidepool/sdk/api/ConfirmationApi.kt new file mode 100644 index 0000000..a65b4ed --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/api/ConfirmationApi.kt @@ -0,0 +1,40 @@ +package org.tidepool.sdk.api + +import org.tidepool.sdk.dto.confirmation.ConfirmationDto +import org.tidepool.sdk.dto.confirmation.ConfirmationLookupDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.PUT +import retrofit2.http.Path + +interface ConfirmationApi { + + @GET("/confirm/invite/{userId}") + suspend fun getPendingCareTeamInvitations( + @Header("X-Tidepool-Session-Token") sessionToken: String, + @Path("userId") userId: String + ): List + + @GET("/confirm/invitations/{userId}") + suspend fun getReceivedInvitations( // TODO: this should return empty list, not error + @Header("X-Tidepool-Session-Token") sessionToken: String, + @Path("userId") userId: String + ): List + + @PUT("/confirm/accept/invite/{userId}/{invitedBy}") + suspend fun acceptCareTeamInvite( + @Header("X-Tidepool-Session-Token") sessionToken: String, + @Path("userId") userId: String, + @Path("invitedBy") invitedBy: String, + @Body requestBody: ConfirmationLookupDto + ) + + @PUT("/confirm/dismiss/invite/{userId}/{invitedBy}") + suspend fun dismissInvite( + @Header("X-Tidepool-Session-Token") sessionToken: String, + @Path("userId") userId: String, + @Path("invitedBy") invitedBy: String, + @Body requestBody: ConfirmationLookupDto + ) +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/requests/Data.kt b/data/src/main/kotlin/org/tidepool/sdk/api/DataApi.kt similarity index 77% rename from lib/src/main/kotlin/org/tidepool/sdk/requests/Data.kt rename to data/src/main/kotlin/org/tidepool/sdk/api/DataApi.kt index aa830b1..311848c 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/requests/Data.kt +++ b/data/src/main/kotlin/org/tidepool/sdk/api/DataApi.kt @@ -1,33 +1,31 @@ -package org.tidepool.sdk.requests +package org.tidepool.sdk.api -import org.tidepool.sdk.model.data.BaseData -import org.tidepool.sdk.model.data.BaseData.DataType +import org.tidepool.sdk.dto.data.BaseDataDto import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Path import retrofit2.http.Query import java.time.Instant -interface Data { - +interface DataApi { + @GET("/data/{userId}") suspend fun getDataForUser( @Header("X-Tidepool-Session-Token") sessionToken: String, @Path("userId") userId: String, @Query("uploadId") uploadId: String? = null, @Query("deviceId") deviceId: String? = null, - @Query("type", encoded = true) types: CommaSeparatedArray? = null, - // TODO: Add subtype + @Query("type", encoded = true) types: CommaSeparatedArray? = null, @Query("startDate") startDate: Instant? = null, @Query("endDate") endDate: Instant? = null, @Query("latest") latest: Boolean? = null, @Query("dexcom") dexcom: Boolean? = null, @Query("carelink") carelink: Boolean? = null, - @Query("medtronic") medtronic: Boolean? = null - ): Array - + @Query("medtronic") medtronic: Boolean? = null, + ): List + class CommaSeparatedArray(private vararg val types: T) { - + override fun toString(): String { var result = "" for (type in types) { @@ -36,5 +34,4 @@ interface Data { return result.substring(0, result.lastIndex) } } - } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/requests/Metadata.kt b/data/src/main/kotlin/org/tidepool/sdk/api/MetadataApi.kt similarity index 57% rename from lib/src/main/kotlin/org/tidepool/sdk/requests/Metadata.kt rename to data/src/main/kotlin/org/tidepool/sdk/api/MetadataApi.kt index 96febd8..6a5c74a 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/requests/Metadata.kt +++ b/data/src/main/kotlin/org/tidepool/sdk/api/MetadataApi.kt @@ -1,22 +1,23 @@ -package org.tidepool.sdk.requests +package org.tidepool.sdk.api -import org.tidepool.sdk.model.metadata.Profile -import org.tidepool.sdk.model.metadata.users.TrustUser +import org.tidepool.sdk.dto.metadata.ProfileDto +import org.tidepool.sdk.dto.metadata.users.TrustUserDto +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Path -interface Metadata { +interface MetadataApi { @GET("/metadata/users/{userId}/users") - suspend fun listUsers( + suspend fun getTrustUsers( @Header("X-Tidepool-Session-Token") sessionToken: String, @Path("userId") userId: String - ): Array + ): List @GET("/metadata/{userId}/profile") - suspend fun getProfile( + suspend fun getUserProfile( @Header("X-Tidepool-Session-Token") sessionToken: String, @Path("userId") userId: String - ): Profile + ): ProfileDto } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/requests/Users.kt b/data/src/main/kotlin/org/tidepool/sdk/api/UserApi.kt similarity index 74% rename from lib/src/main/kotlin/org/tidepool/sdk/requests/Users.kt rename to data/src/main/kotlin/org/tidepool/sdk/api/UserApi.kt index c09086c..5522514 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/requests/Users.kt +++ b/data/src/main/kotlin/org/tidepool/sdk/api/UserApi.kt @@ -1,20 +1,20 @@ -package org.tidepool.sdk.requests +package org.tidepool.sdk.api -import org.tidepool.sdk.model.metadata.users.User +import org.tidepool.sdk.dto.user.UserDto import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Path -interface Users { - +interface UserApi { + @GET("/auth/user") suspend fun getCurrentUserInfo( @Header("X-Tidepool-Session-Token") sessionToken: String - ): User + ): UserDto @GET("/auth/user/{userId}") suspend fun getUserInfo( @Header("X-Tidepool-Session-Token") sessionToken: String, @Path("userId") userId: String - ): User + ): UserDto } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/deserialization/InstantSerializer.kt b/data/src/main/kotlin/org/tidepool/sdk/deserialization/InstantSerializer.kt similarity index 98% rename from lib/src/main/kotlin/org/tidepool/sdk/deserialization/InstantSerializer.kt rename to data/src/main/kotlin/org/tidepool/sdk/deserialization/InstantSerializer.kt index 4ed0c95..0918544 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/deserialization/InstantSerializer.kt +++ b/data/src/main/kotlin/org/tidepool/sdk/deserialization/InstantSerializer.kt @@ -9,14 +9,14 @@ import kotlinx.serialization.encoding.Encoder import java.time.Instant object InstantSerializer : KSerializer { - + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) - + override fun serialize(encoder: Encoder, value: Instant) { encoder.encodeString(value.toString()) } - + override fun deserialize(decoder: Decoder): Instant { return Instant.parse(decoder.decodeString()) } diff --git a/lib/src/main/kotlin/org/tidepool/sdk/deserialization/ResultType.kt b/data/src/main/kotlin/org/tidepool/sdk/deserialization/ResultType.kt similarity index 96% rename from lib/src/main/kotlin/org/tidepool/sdk/deserialization/ResultType.kt rename to data/src/main/kotlin/org/tidepool/sdk/deserialization/ResultType.kt index 291a872..507ba4e 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/deserialization/ResultType.kt +++ b/data/src/main/kotlin/org/tidepool/sdk/deserialization/ResultType.kt @@ -3,6 +3,6 @@ package org.tidepool.sdk.deserialization import kotlin.reflect.KClass interface ResultType { - + val subclassType: KClass } \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/di/DataModule.kt b/data/src/main/kotlin/org/tidepool/sdk/di/DataModule.kt new file mode 100644 index 0000000..e522d28 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/di/DataModule.kt @@ -0,0 +1,94 @@ +package org.tidepool.sdk.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named +import org.koin.dsl.bind +import org.koin.dsl.module +import org.tidepool.sdk.EnvironmentInternal +import org.tidepool.sdk.api.* +import org.tidepool.sdk.deserialization.InstantSerializer +import org.tidepool.sdk.dto.data.* +import org.tidepool.sdk.repository.* +import org.tidepool.sdk.repository.impl.* +import retrofit2.Retrofit +import java.time.Instant + +public val dataModule = module { + + // Session Manager + // JSON Configuration + single { + Json { + ignoreUnknownKeys = true + encodeDefaults = true + isLenient = true + classDiscriminator = + "__type" // Use different discriminator to avoid conflict with 'type' property + serializersModule = SerializersModule { + contextual(Instant::class, InstantSerializer) + // Configure BaseData polymorphism + polymorphic(BaseDataDto::class) { + subclass(BasalAutomatedDataDto::class) + subclass(BolusDataDto::class) + subclass(ContinuousGlucoseDataDto::class) + subclass(DosingDecisionDataDto::class) + subclass(FoodDataDto::class) + subclass(InsulinDataDto::class) + // Add other BaseData subclasses as they get implemented + } + } + } + } + + single { + OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .apply { setLevel(HttpLoggingInterceptor.Level.BODY) } + ) + .build() + } + + // TODO: Retrofit configuration will need Environment class or base URL configuration + // Main API Retrofit instance + single(qualifier = named("main")) { + val environment: EnvironmentInternal = get() + val json: Json = get() + + Retrofit.Builder() + .baseUrl(environment.url) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .client(get()) + .build() + } + + // Auth API Retrofit instance + single(qualifier = named("auth")) { + val environment: EnvironmentInternal = get() + val json: Json = get() + Retrofit.Builder() + .baseUrl(environment.auth.url) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + } + + single { get(qualifier = named("auth")).create(AuthApi::class.java) } + single { get(qualifier = named("main")).create(ConfirmationApi::class.java) } + single { get(qualifier = named("main")).create(DataApi::class.java) } + single { get(qualifier = named("main")).create(MetadataApi::class.java) } + single { get(qualifier = named("main")).create(UserApi::class.java) } + + singleOf(::AuthRepositoryImpl) bind AuthRepository::class + singleOf(::ConfirmationRepositoryImpl) bind ConfirmationRepository::class + singleOf(::DataRepositoryImpl) bind DataRepository::class + singleOf(::MetadataRepositoryImpl) bind MetadataRepository::class + singleOf(::UserRepositoryImpl) bind UserRepository::class +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/AssociationDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/AssociationDto.kt new file mode 100644 index 0000000..eac5f56 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/AssociationDto.kt @@ -0,0 +1,22 @@ +package org.tidepool.sdk.dto + +import kotlinx.serialization.Serializable +import kotlin.reflect.KClass + +@Serializable +data class AssociationDto( + val type: AssociationTypeDto?, + val id: String?, + val url: String?, + val reason: String? +) { + + @Serializable + enum class AssociationTypeDto(val subclassType: KClass) { + + blob(AssociationDto::class), + datum(AssociationDto::class), + image(AssociationDto::class), + url(AssociationDto::class) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/BloodGlucoseDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/BloodGlucoseDto.kt new file mode 100644 index 0000000..ef4e7ce --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/BloodGlucoseDto.kt @@ -0,0 +1,179 @@ +package org.tidepool.sdk.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.BloodGlucoseDto.GlucoseReadingDto +import org.tidepool.sdk.dto.BloodGlucoseDto.UnitsDto.MilligramsPerDeciliter +import org.tidepool.sdk.dto.BloodGlucoseDto.UnitsDto.MillimolesPerLiter +import kotlin.math.roundToInt +import kotlin.time.Duration + +class BloodGlucoseDto { + @Serializable + enum class UnitsDto(private val value: Double, val shorthand: String) { + + @SerialName("mg/dL") + MilligramsPerDeciliter(18.018, "mg/dL"), + + @SerialName("mmol/L") + MillimolesPerLiter(1.0, "mmol/L"); + + fun convert(amount: Double, units: UnitsDto): Double { + if (this == units) { + return amount + } + return amount * units.value / value + } + } + + @Serializable + class GlucoseReadingDto(val amount: Double, val units: UnitsDto) : + Comparable { + + fun inUnit(newUnit: UnitsDto) = units.convert(amount, newUnit) + + operator fun unaryPlus(): GlucoseReadingDto = copy(amount = +this.amount) + operator fun unaryMinus(): GlucoseReadingDto = copy(amount = -this.amount) + operator fun plus(other: GlucoseReadingDto): GlucoseReadingDto = + copy(amount = amount + other.inUnit(units)) + + operator fun minus(other: GlucoseReadingDto): GlucoseReadingDto = + copy(amount = amount - other.inUnit(units)) + + fun copy(amount: Double = this.amount, units: UnitsDto = this.units) = + GlucoseReadingDto(amount, units) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GlucoseReadingDto) return false + return amount.equals(other.inUnit(units)) + } + + override fun hashCode(): Int { + return inUnit(MilligramsPerDeciliter).hashCode() + } + + override fun compareTo(other: GlucoseReadingDto): Int { + return amount.compareTo(other.inUnit(units)) + } + + private fun Double.roundMillimolesPerLiter(): Double { + return (this * 10).roundToInt() / 10.0 + } + + fun toString(unit: UnitsDto): String { + return when (unit) { + MillimolesPerLiter -> inUnit(unit).roundMillimolesPerLiter().toString() + MilligramsPerDeciliter -> inUnit(unit).roundToInt().toString() + } + } + + fun toSignString(unit: UnitsDto): String { + return when (unit) { + MillimolesPerLiter -> inUnit(unit).let { + if (it == 0.0) "0" else "%+.1f".format( + it.roundMillimolesPerLiter() + ) + } + + MilligramsPerDeciliter -> inUnit(unit).let { amount -> + amount.roundToInt().let { if (it == 0) "0" else "%+d".format(it) } + } + } + } + } + + @Serializable + enum class TrendDto { + + @SerialName("constant") + Constant, + + @SerialName("slowFall") + SlowFall, + + @SerialName("slowRise") + SlowRise, + + @SerialName("moderateFall") + ModerateFall, + + @SerialName("moderateRise") + ModerateRise, + + @SerialName("rapidFall") + RapidFall, + + @SerialName("rapidRise") + RapidRise, + } + + @Serializable + data class TargetDto( + val target: Double?, + val range: Double?, + val low: Double?, + val high: Double? + ) + + @Serializable + data class StartTargetDto( + val start: Duration?, + val target: Double?, + val range: Double?, + val low: Double?, + val high: Double? + ) + + companion object { + + private fun UnitsDto.valueRange(): ClosedRange { + return when (this) { + MilligramsPerDeciliter -> 0.0..1000.0 + MillimolesPerLiter -> 0.0..55.0 + } + } + + fun clamp(value: Double, units: UnitsDto): Double { + return value.coerceIn(units.valueRange()) + } + } +} + +internal val Int.mgdlDto: GlucoseReadingDto + get() = GlucoseReadingDto( + toDouble(), + MilligramsPerDeciliter + ) +internal val Long.mgdlDto: GlucoseReadingDto + get() = GlucoseReadingDto( + toDouble(), + MilligramsPerDeciliter + ) +internal val Float.mgdlDto: GlucoseReadingDto + get() = GlucoseReadingDto( + toDouble(), + MilligramsPerDeciliter + ) +internal val Double.mgdlDto: GlucoseReadingDto + get() = GlucoseReadingDto( + this, + MilligramsPerDeciliter + ) + +internal val Int.mmollDto: GlucoseReadingDto + get() = GlucoseReadingDto( + toDouble(), + MillimolesPerLiter + ) +internal val Long.mmollDto: GlucoseReadingDto + get() = GlucoseReadingDto( + toDouble(), + MillimolesPerLiter + ) +internal val Float.mmollDto: GlucoseReadingDto + get() = GlucoseReadingDto( + toDouble(), + MillimolesPerLiter + ) +internal val Double.mmollDto: GlucoseReadingDto get() = GlucoseReadingDto(this, MillimolesPerLiter) \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/auth/RealmDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/auth/RealmDto.kt new file mode 100644 index 0000000..e517a23 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/auth/RealmDto.kt @@ -0,0 +1,32 @@ +package org.tidepool.sdk.dto.auth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class RealmDto { + + @SerialName("dev1") + Dev1, + + @SerialName("qa1") + Qa1, + + @SerialName("qa2") + Qa2, + + @SerialName("qa3") + Qa3, + + @SerialName("qa4") + Qa4, + + @SerialName("qa5") + Qa5, + + @SerialName("integration") + Integration, + + @SerialName("tidepool") + Tidepool +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/requests/TokenRequest.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/auth/TokenRequestDto.kt similarity index 60% rename from lib/src/main/kotlin/org/tidepool/sdk/requests/TokenRequest.kt rename to data/src/main/kotlin/org/tidepool/sdk/dto/auth/TokenRequestDto.kt index 24b54ff..e0d2ca0 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/requests/TokenRequest.kt +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/auth/TokenRequestDto.kt @@ -1,83 +1,84 @@ -package org.tidepool.sdk.requests; +package org.tidepool.sdk.dto.auth import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class TokenRequest( - val grant_type: GrantType, +data class TokenRequestDto( + val grant_type: GrantTypeDto, val client_id: String, val client_secret: String? = null, val subject_token: String? = null, - val subject_token_type: SubjectTokenType? = null, - val requested_token_type: RequestedTokenType? = null, + val subject_token_type: SubjectTokenTypeDto? = null, + val requested_token_type: RequestedTokenTypeDto? = null, val subject_issuer: String? = null, val username: String? = null, val password: String? = null, val code: String? = null, - val code_verifier: String? = null + val code_verifier: String? = null, ) { - + companion object { - + fun createWithAuthorizationCode( client_id: String, code: String, - initializer: Builder.() -> Unit = {} - ): TokenRequest { - return Builder(GrantType.authorization_code, client_id).apply { + initializer: Builder.() -> Unit = {}, + ): TokenRequestDto { + return Builder(GrantTypeDto.AuthorizationCode, client_id).apply { this.code = code initializer() }.build() } - + fun createWithRefreshToken( client_id: String, - initializer: Builder.() -> Unit = {} - ): TokenRequest { - return Builder(GrantType.refresh_token, client_id).apply { + initializer: Builder.() -> Unit = {}, + ): TokenRequestDto { + return Builder(GrantTypeDto.RefreshToken, client_id).apply { initializer() }.build() } - + fun createWithPassword( client_id: String, username: String, password: String, - initializer: Builder.() -> Unit = {} - ): TokenRequest { - return Builder(GrantType.password, client_id).apply { + initializer: Builder.() -> Unit = {}, + ): TokenRequestDto { + return Builder(GrantTypeDto.Password, client_id).apply { this.username = username this.password = password initializer() }.build() } - + fun createWithTokenExchange( client_id: String, subject_token: String, - initializer: Builder.() -> Unit = {} - ): TokenRequest { - return Builder(GrantType.tokenExchange, client_id).apply { + initializer: Builder.() -> Unit = {}, + ): TokenRequestDto { + return Builder(GrantTypeDto.TokenExchange, client_id).apply { this.subject_token = subject_token initializer() }.build() } } - class Builder(val grant_type: GrantType, val client_id: String) { - + class Builder(val grant_type: GrantTypeDto, val client_id: String) { + var client_secret: String? = null var subject_token: String? = null - var subject_token_type: SubjectTokenType? = null - var requested_token_type: RequestedTokenType? = null + var subject_token_type: SubjectTokenTypeDto? = null + var requested_token_type: RequestedTokenTypeDto? = null var subject_issuer: String? = null var username: String? = null var password: String? = null var code: String? = null var code_verifier: String? = null - internal fun build(): TokenRequest { - return TokenRequest( + + fun build(): TokenRequestDto { + return TokenRequestDto( grant_type, client_id, client_secret, @@ -95,31 +96,36 @@ data class TokenRequest( } @Serializable -enum class GrantType { - authorization_code, - refresh_token, - password, - +enum class GrantTypeDto { + @SerialName("authorization_code") + AuthorizationCode, + + @SerialName("refresh_token") + RefreshToken, + + @SerialName("password") + Password, + @SerialName("urn:ietf:params:oauth:grant-type:token-exchange") - tokenExchange + TokenExchange } @Serializable -enum class SubjectTokenType { - +enum class SubjectTokenTypeDto { + @SerialName("urn:ietf:params:oauth:token-type:access_token") - access_token, - + AccessToken, + @SerialName("urn:ietf:params:oauth:token-type:jwt") - jwt + Jwt } @Serializable -enum class RequestedTokenType { - +enum class RequestedTokenTypeDto { + @SerialName("urn:ietf:params:oauth:token-type:access_token") access_token, - + @SerialName("urn:ietf:params:oauth:token-type:refresh_token") refresh_token } \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/auth/TokenResponseDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/auth/TokenResponseDto.kt new file mode 100644 index 0000000..0ce4ff2 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/auth/TokenResponseDto.kt @@ -0,0 +1,12 @@ +package org.tidepool.sdk.dto.auth + +import kotlinx.serialization.Serializable + +@Serializable +data class TokenResponseDto( + val expires_in: Int, + val access_token: String, + val id_token: String, + val token_type: String, + val refresh_token: String? +) \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationDto.kt new file mode 100644 index 0000000..9fccfd2 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationDto.kt @@ -0,0 +1,29 @@ +package org.tidepool.sdk.dto.confirmation + +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import org.tidepool.sdk.dto.metadata.ProfileDto +import java.time.Instant + +@Serializable +data class ConfirmationDto( + val key: String = "", + val type: ConfirmationTypeDto = ConfirmationTypeDto.SignupConfirmation, + val status: ConfirmationStatusDto = ConfirmationStatusDto.Pending, + val email: String = "", + val creatorId: String = "", + @Contextual val created: Instant = Instant.now(), + @Contextual val modified: Instant? = null, + val creator: CreatorDto? = null, + val context: JsonObject? = null, // no context for context :3 + val restrictions: RestrictionsDto? = null, + @Contextual val expiresAt: Instant? = null, +) { + + @Serializable + data class CreatorDto( + val userId: String = "", + val profile: ProfileDto = ProfileDto(), + ) +} diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationLookupDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationLookupDto.kt new file mode 100644 index 0000000..c7d0976 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationLookupDto.kt @@ -0,0 +1,5 @@ +package org.tidepool.sdk.dto.confirmation + +data class ConfirmationLookupDto( + val key: String, +) \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationStatusDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationStatusDto.kt new file mode 100644 index 0000000..0acea9c --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationStatusDto.kt @@ -0,0 +1,16 @@ +package org.tidepool.sdk.dto.confirmation + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class ConfirmationStatusDto { + @SerialName("pending") + Pending, + @SerialName("completed") + Completed, + @SerialName("canceled") + Canceled, + @SerialName("declined") + Declined +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationTypeDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationTypeDto.kt new file mode 100644 index 0000000..d202e47 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/ConfirmationTypeDto.kt @@ -0,0 +1,17 @@ +package org.tidepool.sdk.dto.confirmation + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class ConfirmationTypeDto { + @SerialName("password_reset") + PasswordReset, + @SerialName("careteam_invitation") + CareteamInvitation, + @SerialName("signup_confirmation") + SignupConfirmation, + @SerialName("no_account") + NoAccount, + ; +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/RestrictionsDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/RestrictionsDto.kt new file mode 100644 index 0000000..7f00e20 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/confirmation/RestrictionsDto.kt @@ -0,0 +1,9 @@ +package org.tidepool.sdk.dto.confirmation + +import kotlinx.serialization.Serializable + +@Serializable +data class RestrictionsDto( + val canAccept: Boolean, + val requiredIdp: String? +) \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/BasalAutomatedDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/BasalAutomatedDataDto.kt new file mode 100644 index 0000000..9b5e142 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/BasalAutomatedDataDto.kt @@ -0,0 +1,36 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +// TODO: Finish implementing automated.v1 +@Serializable +data class BasalAutomatedDataDto( + val deliveryType: DeliveryTypeDto, + val duration: Int, + val expectedDuration: Int? = null, + val rate: Double = -1.0, + val scheduleName: String? = null, +) : BaseDataDto(DataTypeDto.Basal) { + + val insulinFormulation: Nothing + get() = TODO("schema \"formulation.v1\" not implemented") + val suppressed: Nothing + get() = TODO("schema \"scheduled.v1\" not implemented") + + @Serializable + enum class DeliveryTypeDto { + + @SerialName("automated") + Automated, + + @SerialName("scheduled") + Scheduled, + + @SerialName("suspend") + Suspend, + + @SerialName("temp") + Temp, + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/BaseDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/BaseDataDto.kt new file mode 100644 index 0000000..1f6a16f --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/BaseDataDto.kt @@ -0,0 +1,91 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Contextual +import org.tidepool.sdk.deserialization.ResultType +import org.tidepool.sdk.dto.AssociationDto +import java.time.Instant +import java.util.TimeZone +import kotlin.reflect.KClass +import kotlin.time.Duration + +// TODO: finish implementing base.v1 +@Serializable +sealed class BaseDataDto( + val type: DataTypeDto = DataTypeDto.Alert, + @Contextual val time: Instant? = null, + val annotations: Array>? = null, + val associations: Array? = null, + @Contextual val clockDriftOffset: Duration? = null, + @Contextual val conversionOffset: Duration? = null, + val dataSetId: String? = null, + val deviceTime: String? = null, + val id: String? = null, + val notes: Array? = null, + @Contextual val timeZone: TimeZone? = null, + @Contextual val timeZoneOffset: Duration? = null +) { + + val location: Nothing + get() = TODO("schema \"\" not implemented") + + @Serializable + enum class DataTypeDto(override val subclassType: KClass) : + ResultType { + + @SerialName("alert") + Alert(BaseDataDto::class), + + @SerialName("basal") + Basal(BasalAutomatedDataDto::class), + + @SerialName("bloodKetone") + BloodKetone(BaseDataDto::class), + + @SerialName("bolus") + Bolus(BolusDataDto::class), + + @SerialName("wizard") + Calculator(BaseDataDto::class), + + @SerialName("cbg") + Cbg(ContinuousGlucoseDataDto::class), + + @SerialName("cgmSettings") + CgmSettings(BaseDataDto::class), + + @SerialName("controllerSettings") + ControllerSettings(BaseDataDto::class), + + @SerialName("controllerStatus") + ControllerStatus(BaseDataDto::class), + + @SerialName("deviceEvent") + DeviceEvent(BaseDataDto::class), + + @SerialName("dosingDecision") + DosingDecision(DosingDecisionDataDto::class), + + @SerialName("food") + Food(FoodDataDto::class), + + @SerialName("insulin") + Insulin(InsulinDataDto::class), + + @SerialName("physicalActivity") + PhysicalActivity(BaseDataDto::class), + + @SerialName("pumpSettings") + PumpSettings(BaseDataDto::class), + + @SerialName("pumpStatus") + PumpStatus(BaseDataDto::class), + + @SerialName("reportedState") + ReportedState(BaseDataDto::class), + + @SerialName("smbg") + Smbg(BaseDataDto::class) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/BolusDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/BolusDataDto.kt new file mode 100644 index 0000000..c149f1d --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/BolusDataDto.kt @@ -0,0 +1,38 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +// schema bolus.v1 +// line 2330 +@Serializable +data class BolusDataDto( + val subType: BolusSubtypeDto = BolusSubtypeDto.Normal, + val deliveryContext: DeliveryContextDto, +) : BaseDataDto(DataTypeDto.Bolus) { + + val insulinFormulation: Nothing + get() = TODO("schema \"formulation.v1\" not implemented") +} + +@Serializable +enum class BolusSubtypeDto { + @SerialName("automated") + Automated, + @SerialName("dual/square") + DualSquare, + @SerialName("normal") + Normal, + @SerialName("square") + Square, + ; +} + +@Serializable +enum class DeliveryContextDto { + Device, + Algorithm, + Remote, + Undetermined, + ; +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/ContinuousGlucoseDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/ContinuousGlucoseDataDto.kt new file mode 100644 index 0000000..e386aed --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/ContinuousGlucoseDataDto.kt @@ -0,0 +1,34 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.BloodGlucoseDto +import org.tidepool.sdk.dto.BloodGlucoseDto.GlucoseReadingDto + +@Serializable +data class ContinuousGlucoseDataDto( + val value: Double? = null, + val units: BloodGlucoseDto.UnitsDto? = null, + val trend: BloodGlucoseDto.TrendDto? = null, + val trendRate: Double? = null +) : BaseDataDto(type = DataTypeDto.Cbg) { + + public constructor( + reading: GlucoseReadingDto?, + trend: BloodGlucoseDto.TrendDto?, + trendRate: Double? + ) : this(reading?.amount, reading?.units, trend, trendRate) + + val reading: GlucoseReadingDto? by lazy { + value?.let { value -> + units?.let { units -> + GlucoseReadingDto(value, units) + } + } + } + + fun copy( + reading: GlucoseReadingDto? = this.reading, + trend: BloodGlucoseDto.TrendDto? = this.trend, + trendRate: Double? = this.trendRate + ) = copy(reading?.amount, reading?.units, trend, trendRate) +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/DosingDecisionDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/DosingDecisionDataDto.kt new file mode 100644 index 0000000..0b1697b --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/DosingDecisionDataDto.kt @@ -0,0 +1,90 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Contextual +import org.tidepool.sdk.dto.BloodGlucoseDto +import java.time.Instant + +// TODO: finish implementing dosingdecision.v1 +@Serializable +data class DosingDecisionDataDto( + val reason: String, + val carbsOnBoard: CarbsOnBoardDto? = null, + val insulinOnBoard: InsulinOnBoardDto? = null, + val recommendedBasal: RecommendedBasalDto? = null, + val recommendedBolus: RecommendedBolusDto? = null, + val requestedBolus: RequestedBolusDto? = null, + val scheduleTimeZoneOffset: Int? = null, + val units: UnitsDto = UnitsDto(), +) : BaseDataDto(DataTypeDto.DosingDecision) { + + val originalFood: Nothing + get() = TODO("backing object not implemented") + val food: Nothing + get() = TODO("backing object not implemented") + val smbg: Nothing + get() = TODO("schema \"bloodglucose.v1\" not implemented") + val bgTargetSchedule: Nothing + get() = TODO("schema \"targetstart.v1\" not implemented") + val bgHistorical: Nothing + get() = TODO("schema \"bloodglucose.v1\" not implemented") + val bgForecast: Nothing + get() = TODO("schema \"bloodglucose.v1\" not implemented") + val warnings: Nothing + get() = TODO("schema \"issue.v1\" not implemented") + val errors: Nothing + get() = TODO("schema \"issue.v1\" not implemented") + + @Serializable + data class CarbsOnBoardDto( + @Contextual val time: Instant? = null, + val amount: Double = -1.0, + ) + + @Serializable + data class InsulinOnBoardDto( + @Contextual val time: Instant? = null, + val amount: Double = -1.0, + ) + + @Serializable + data class RecommendedBasalDto( + val rate: Double = -1.0, + val duration: Double? = null, + ) + + @Serializable + data class RecommendedBolusDto( + val amount: Double = -1.0, + ) + + @Serializable + data class RequestedBolusDto( + val amount: Double = -1.0, + ) + + @Serializable + data class UnitsDto( + val bg: BloodGlucoseDto.UnitsDto = BloodGlucoseDto.UnitsDto.MilligramsPerDeciliter, + val carb: CarbDto = CarbDto.Exchanges, + val insulin: InsulinDto = InsulinDto.Units, + ) { + + @Serializable + enum class CarbDto { + + @SerialName("exchanges") + Exchanges, + + @SerialName("grams") + Grams + } + + @Serializable + enum class InsulinDto { + + Units, + } + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/FoodDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/FoodDataDto.kt new file mode 100644 index 0000000..bd7ba08 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/FoodDataDto.kt @@ -0,0 +1,41 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +// schema food.v1 +@Serializable +data class FoodDataDto( + val brand: String? = null, + val code: String? = null, + val meal: MealDto? = null, + val mealOther: String? = null, + val name: String? = null, +) : BaseDataDto(DataTypeDto.Food) { + + val amount: Nothing + get() = TODO("schema \"amount.v1\" not implemented") + val ingredients: Nothing + get() = TODO("schema \"ingredientarray.v1\" not implemented") + val nutrition: Nothing + get() = TODO("schema \"nutrition.v1\" not implemented") + + @Serializable + enum class MealDto { + + @SerialName("breakfast") + breakfast, + + @SerialName("lunch") + lunch, + + @SerialName("dinner") + dinner, + + @SerialName("snack") + snack, + + @SerialName("other") + other, + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/InsulinDataDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/InsulinDataDto.kt new file mode 100644 index 0000000..8a7cae5 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/InsulinDataDto.kt @@ -0,0 +1,14 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.Serializable + +// schema insulin.v1 +@Serializable +data class InsulinDataDto( + val dose: DoseDto, + val site: String? +) : BaseDataDto(DataTypeDto.Insulin) { + + val formulation: Nothing + get() = TODO("schema \"formulation.v1\" not implemented") +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/data/InsulinDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/data/InsulinDto.kt new file mode 100644 index 0000000..b482bc2 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/data/InsulinDto.kt @@ -0,0 +1,23 @@ +package org.tidepool.sdk.dto.data + +import kotlinx.serialization.Serializable + +@Serializable +class InsulinDto { + + @Serializable + enum class UnitsDto { + + Units + } +} + +// schema dose.v1 +@Serializable +data class DoseDto( + val units: InsulinDto.UnitsDto, + val total: Double, + val food: Double?, + val correction: Double?, + val active: Double?, +) diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/ProfileDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/ProfileDto.kt new file mode 100644 index 0000000..d6b9f95 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/ProfileDto.kt @@ -0,0 +1,8 @@ +package org.tidepool.sdk.dto.metadata + +import kotlinx.serialization.Serializable + +@Serializable +open class ProfileDto( + val fullName: String? = null, +) \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/users/PermissionsDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/users/PermissionsDto.kt new file mode 100644 index 0000000..fe7215b --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/users/PermissionsDto.kt @@ -0,0 +1,36 @@ +package org.tidepool.sdk.dto.metadata.users + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +@Serializable +enum class PermissionDto { + @SerialName("custodian") + Custodian, + @SerialName("view") + View, + @SerialName("note") + Note, + @SerialName("upload") + Upload, + ; +} + +@Serializable +data class PermissionsDto( + val custodian: JsonObject? = null, + val view: JsonObject? = null, + val note: JsonObject? = null, + val upload: JsonObject? = null +) { + + val permissionsSet: Set by lazy { + setOfNotNull( + PermissionDto.Custodian.takeUnless { custodian == null }, + PermissionDto.View.takeUnless { view == null }, + PermissionDto.Note.takeUnless { note == null }, + PermissionDto.Upload.takeUnless { upload == null }, + ) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/users/TrustUserDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/users/TrustUserDto.kt new file mode 100644 index 0000000..a086751 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/metadata/users/TrustUserDto.kt @@ -0,0 +1,34 @@ +package org.tidepool.sdk.dto.metadata.users + +import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.user.UserDto + +/** + * DTO for trust user data that can contain either trustor or trustee permissions + */ +@Serializable +data class TrustUserDto( + val trustorPermissions: PermissionsDto? = null, + val trusteePermissions: PermissionsDto? = null +) : UserDto() { + + /** + * Gets the permissions regardless of whether this is a trustor or trustee + */ + val permissions: Set + get() = trustorPermissions?.permissionsSet ?: trusteePermissions?.permissionsSet + ?: emptySet() + + /** + * Indicates whether this is a trustor user (has trustorPermissions) + */ + val isTrustor: Boolean + get() = trustorPermissions != null + + /** + * Indicates whether this is a trustee user (has trusteePermissions) + */ + val isTrustee: Boolean + get() = trusteePermissions != null + +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/dto/user/UserDto.kt b/data/src/main/kotlin/org/tidepool/sdk/dto/user/UserDto.kt new file mode 100644 index 0000000..c44b351 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/dto/user/UserDto.kt @@ -0,0 +1,26 @@ +package org.tidepool.sdk.dto.user + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import org.tidepool.sdk.dto.metadata.ProfileDto +import java.time.Instant + +@Serializable +open class UserDto( + val emailVerified: Boolean = false, + val emails: List? = null, + @Contextual val termsAccepted: Instant? = null, + @SerialName("userid") + val userId: String = "", + @SerialName("username") + val userName: String? = null, + val roles: List? = null, + @Contextual val createdTime: Instant? = null, + val createdUserId: String? = null, + @Contextual val modifiedTime: Instant? = null, + val modifiedUserId: String? = null, + @Contextual val deletedTime: Instant? = null, + val deletedUserId: String? = null, + val profile: ProfileDto? = null, +) diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/AuthRepository.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/AuthRepository.kt new file mode 100644 index 0000000..67f0234 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/AuthRepository.kt @@ -0,0 +1,23 @@ +package org.tidepool.sdk.repository + +import org.tidepool.sdk.dto.auth.TokenRequestDto +import org.tidepool.sdk.dto.auth.TokenResponseDto +import org.tidepool.sdk.dto.auth.RealmDto + +interface AuthRepository { + + suspend fun obtainToken( + realm: RealmDto, + tokenRequest: TokenRequestDto, + ): Result + + suspend fun authorize( + realm: RealmDto, + clientId: String, + scopes: Array, + redirectUri: String, + loginHint: String? = null, + kcIdpHint: String? = null, + prompt: String? = null, + ): Result +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/ConfirmationRepository.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/ConfirmationRepository.kt new file mode 100644 index 0000000..ae541b4 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/ConfirmationRepository.kt @@ -0,0 +1,25 @@ +package org.tidepool.sdk.repository + +import org.tidepool.sdk.dto.confirmation.ConfirmationDto + +interface ConfirmationRepository { + + suspend fun getReceivedInvitations( + sessionToken: String, + userId: String, + ): Result> + + suspend fun acceptConfirmation( + sessionToken: String, + userId: String, + confirmationKey: String, + creatorId: String, + ): Result + + suspend fun dismissConfirmation( + sessionToken: String, + userId: String, + confirmationKey: String, + creatorId: String, + ): Result +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/DataRepository.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/DataRepository.kt new file mode 100644 index 0000000..ef99232 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/DataRepository.kt @@ -0,0 +1,21 @@ +package org.tidepool.sdk.repository + +import org.tidepool.sdk.dto.data.BaseDataDto +import java.time.Instant + +interface DataRepository { + + suspend fun getDataForUser( + userId: String, + uploadId: String? = null, + deviceId: String? = null, + types: List? = null, + startDate: Instant? = null, + endDate: Instant? = null, + latest: Boolean? = null, + dexcom: Boolean? = null, + carelink: Boolean? = null, + medtronic: Boolean? = null, + sessionToken: String, + ): Result> +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/MetadataRepository.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/MetadataRepository.kt new file mode 100644 index 0000000..4ad2a45 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/MetadataRepository.kt @@ -0,0 +1,9 @@ +package org.tidepool.sdk.repository + +import org.tidepool.sdk.dto.metadata.users.TrustUserDto + +interface MetadataRepository { + + suspend fun getTrustUsers(sessionToken: String): Result> + +} diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/UserRepository.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/UserRepository.kt new file mode 100644 index 0000000..9eae7f7 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/UserRepository.kt @@ -0,0 +1,13 @@ +package org.tidepool.sdk.repository + +import org.tidepool.sdk.dto.user.UserDto + +interface UserRepository { + + suspend fun getCurrentUser(sessionToken: String): Result + + suspend fun getUser( + userId: String, + sessionToken: String, + ): Result +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/impl/AuthRepositoryImpl.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/AuthRepositoryImpl.kt new file mode 100644 index 0000000..ef2bfc3 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/AuthRepositoryImpl.kt @@ -0,0 +1,42 @@ +package org.tidepool.sdk.repository.impl + +import org.tidepool.sdk.api.AuthApi +import org.tidepool.sdk.dto.auth.TokenRequestDto +import org.tidepool.sdk.dto.auth.TokenResponseDto +import org.tidepool.sdk.dto.auth.RealmDto +import org.tidepool.sdk.repository.AuthRepository + +class AuthRepositoryImpl( + private val authApi: AuthApi, +) : AuthRepository { + + override suspend fun obtainToken( + realm: RealmDto, + tokenRequest: TokenRequestDto, + ): Result { + return try { + val response = authApi.obtainToken(realm, tokenRequest) + Result.success(response) + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun authorize( + realm: RealmDto, + clientId: String, + scopes: Array, + redirectUri: String, + loginHint: String?, + kcIdpHint: String?, + prompt: String?, + ): Result { + return try { + // Note: This would need proper implementation based on actual API requirements + // The current API returns Unit, but typically authorization returns a redirect URL + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/impl/ConfirmationRepositoryImpl.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/ConfirmationRepositoryImpl.kt new file mode 100644 index 0000000..f0d6837 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/ConfirmationRepositoryImpl.kt @@ -0,0 +1,54 @@ +package org.tidepool.sdk.repository.impl + +import org.tidepool.sdk.api.ConfirmationApi +import org.tidepool.sdk.dto.confirmation.ConfirmationDto +import org.tidepool.sdk.dto.confirmation.ConfirmationLookupDto +import org.tidepool.sdk.repository.ConfirmationRepository +import org.tidepool.sdk.runCatchingNetworkExceptions + +class ConfirmationRepositoryImpl( + private val confirmationApi: ConfirmationApi, +) : ConfirmationRepository { + + override suspend fun getReceivedInvitations( + sessionToken: String, + userId: String, + ): Result> = runCatchingNetworkExceptions { + confirmationApi.getReceivedInvitations( + sessionToken = sessionToken, + userId = userId, + ) + } + + override suspend fun acceptConfirmation( + sessionToken: String, + userId: String, + confirmationKey: String, + creatorId: String, + ) = runCatchingNetworkExceptions { + confirmationApi.acceptCareTeamInvite( + sessionToken = sessionToken, + userId = userId, + invitedBy = creatorId, + requestBody = ConfirmationLookupDto( + key = confirmationKey, + ), + ) + } + + override suspend fun dismissConfirmation( + sessionToken: String, + userId: String, + confirmationKey: String, + creatorId: String, + ): Result = runCatchingNetworkExceptions { + confirmationApi.dismissInvite( + sessionToken = sessionToken, + userId = userId, + invitedBy = creatorId, + requestBody = ConfirmationLookupDto( + key = confirmationKey, + ), + ) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/impl/DataRepositoryImpl.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/DataRepositoryImpl.kt new file mode 100644 index 0000000..2fa015f --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/DataRepositoryImpl.kt @@ -0,0 +1,44 @@ +package org.tidepool.sdk.repository.impl + +import org.tidepool.sdk.api.DataApi +import org.tidepool.sdk.dto.data.BaseDataDto +import org.tidepool.sdk.repository.DataRepository +import org.tidepool.sdk.runCatchingNetworkExceptions +import java.time.Instant + +class DataRepositoryImpl( + private val dataApi: DataApi, +) : DataRepository { + + override suspend fun getDataForUser( + userId: String, + uploadId: String?, + deviceId: String?, + types: List?, + startDate: Instant?, + endDate: Instant?, + latest: Boolean?, + dexcom: Boolean?, + carelink: Boolean?, + medtronic: Boolean?, + sessionToken: String + ) = runCatchingNetworkExceptions { + val typesParam = types + ?.toTypedArray() + ?.let { DataApi.CommaSeparatedArray(*it) } + + dataApi.getDataForUser( + sessionToken = sessionToken, + userId = userId, + uploadId = uploadId, + deviceId = deviceId, + types = typesParam, + startDate = startDate, + endDate = endDate, + latest = latest, + dexcom = dexcom, + carelink = carelink, + medtronic = medtronic + ) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/impl/MetadataRepositoryImpl.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/MetadataRepositoryImpl.kt new file mode 100644 index 0000000..f3690ff --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/MetadataRepositoryImpl.kt @@ -0,0 +1,21 @@ +package org.tidepool.sdk.repository.impl + +import org.tidepool.sdk.api.MetadataApi +import org.tidepool.sdk.api.UserApi +import org.tidepool.sdk.repository.MetadataRepository +import org.tidepool.sdk.runCatchingNetworkExceptions + +class MetadataRepositoryImpl( + private val userApi: UserApi, + private val metadataApi: MetadataApi, +) : MetadataRepository { + + override suspend fun getTrustUsers(sessionToken: String) = runCatchingNetworkExceptions { + val userId = userApi.getCurrentUserInfo(sessionToken).userId + + metadataApi.getTrustUsers( + sessionToken = sessionToken, + userId = userId, + ) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/org/tidepool/sdk/repository/impl/UserRepositoryImpl.kt b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/UserRepositoryImpl.kt new file mode 100644 index 0000000..5c6b1f6 --- /dev/null +++ b/data/src/main/kotlin/org/tidepool/sdk/repository/impl/UserRepositoryImpl.kt @@ -0,0 +1,27 @@ +package org.tidepool.sdk.repository.impl + +import org.tidepool.sdk.api.UserApi +import org.tidepool.sdk.dto.user.UserDto +import org.tidepool.sdk.repository.UserRepository +import org.tidepool.sdk.runCatchingNetworkExceptions + +class UserRepositoryImpl( + private val userApi: UserApi, +) : UserRepository { + + override suspend fun getCurrentUser(sessionToken: String): Result = + runCatchingNetworkExceptions { + userApi.getCurrentUserInfo(sessionToken) + } + + + override suspend fun getUser( + userId: String, + sessionToken: String, + ) = runCatchingNetworkExceptions { + userApi.getUserInfo( + sessionToken = sessionToken, + userId = userId, + ) + } +} \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 7ab5903..9aaa720 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -9,6 +9,7 @@ plugins { // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. kotlin("jvm") version "2.2.0" kotlin("plugin.serialization") version "2.2.0" + id("com.google.devtools.ksp") version "2.2.20-2.0.3" // Apply the java-library plugin for API and implementation separation. `java-library` } @@ -16,9 +17,12 @@ plugins { repositories { // Use Maven Central for resolving dependencies. mavenCentral() + google() } dependencies { + implementation(project(":data")) + implementation("com.squareup.retrofit2:retrofit:3.0.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") @@ -28,9 +32,15 @@ dependencies { // OkHttp for MediaType implementation("com.squareup.okhttp3:okhttp:5.1.0") + + // Room KMP dependencies + implementation("androidx.room:room-runtime:2.8.1") + implementation("androidx.sqlite:sqlite-bundled:2.5.0") + add("ksp", "androidx.room:room-compiler:2.8.1") - // Remove Gson converter - keeping commented for reference during migration - // api("com.squareup.retrofit2:converter-gson:2.11.0") + // Koin dependency injection + implementation("io.insert-koin:koin-core:4.1.0") + implementation("io.insert-koin:koin-annotations:2.1.0") // Use the Kotlin JUnit 5 integration. testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") @@ -38,6 +48,10 @@ dependencies { // Use the JUnit 5 integration. testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2") + // Koin testing + testImplementation("io.insert-koin:koin-test:4.1.0") + testImplementation("io.insert-koin:koin-test-junit5:4.1.0") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/lib/src/main/kotlin/org/tidepool/sdk/CommunicationHelper.kt b/lib/src/main/kotlin/org/tidepool/sdk/CommunicationHelper.kt deleted file mode 100644 index 991f940..0000000 --- a/lib/src/main/kotlin/org/tidepool/sdk/CommunicationHelper.kt +++ /dev/null @@ -1,93 +0,0 @@ -package org.tidepool.sdk - -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclass -import okhttp3.MediaType.Companion.toMediaType -import org.tidepool.sdk.deserialization.InstantSerializer -import org.tidepool.sdk.model.data.BasalAutomatedData -import org.tidepool.sdk.model.data.BaseData -import org.tidepool.sdk.model.data.BolusData -import org.tidepool.sdk.model.data.ContinuousGlucoseData -import org.tidepool.sdk.model.data.DosingDecisionData -import org.tidepool.sdk.model.data.FoodData -import org.tidepool.sdk.model.data.InsulinData -import org.tidepool.sdk.model.metadata.users.TrustUser -import org.tidepool.sdk.model.metadata.users.TrusteeUser -import org.tidepool.sdk.model.metadata.users.TrustorUser -import org.tidepool.sdk.requests.Auth -import org.tidepool.sdk.requests.Confirmations -import org.tidepool.sdk.requests.Data -import org.tidepool.sdk.requests.Metadata -import org.tidepool.sdk.requests.Users -import retrofit2.Retrofit -import java.time.Instant - -public class CommunicationHelper(private val environment: Environment) { - - private val retrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl(environment.url) - .addConverterFactory(jsonConfig.asConverterFactory("application/json".toMediaType())) - .build() - } - - private val authRetrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl(environment.auth.url) - .addConverterFactory(jsonConfig.asConverterFactory("application/json".toMediaType())) - .build() - } - - companion object { - - public val jsonConfig: Json by lazy { - Json { - ignoreUnknownKeys = true - encodeDefaults = true - isLenient = true - classDiscriminator = - "__type" // Use different discriminator to avoid conflict with 'type' property - serializersModule = SerializersModule { - contextual(Instant::class, InstantSerializer) - polymorphic(TrustUser::class) { - subclass(TrusteeUser::class) - subclass(TrustorUser::class) - } - // Configure BaseData polymorphism - polymorphic(BaseData::class) { - subclass(BasalAutomatedData::class) - subclass(BolusData::class) - subclass(ContinuousGlucoseData::class) - subclass(DosingDecisionData::class) - subclass(FoodData::class) - subclass(InsulinData::class) - // Add other BaseData subclasses as they get implemented - } - } - } - } - } - - public val auth: Auth by lazy { - authRetrofit.create(Auth::class.java) - } - - public val data: Data by lazy { - retrofit.create(Data::class.java) - } - - public val users: Users by lazy { - retrofit.create(Users::class.java) - } - - public val metadata: Metadata by lazy { - retrofit.create(Metadata::class.java) - } - - public val confirmations: Confirmations by lazy { - retrofit.create(Confirmations::class.java) - } -} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/Environment.kt b/lib/src/main/kotlin/org/tidepool/sdk/Environment.kt index 891e06a..9b6d38e 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/Environment.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/Environment.kt @@ -1,32 +1,44 @@ package org.tidepool.sdk -import java.net.URL - interface Environment { - val url: URL val envCode: String - get() { - return toString() - } + get() = toString() val auth: AuthenticationServer } -public enum class Environments( - url: String, +enum class Environments( override val envCode: String, override val auth: AuthenticationServer -) : Environment { - - Production("https://api.tidepool.org", "tidepool", AuthenticationServers.Production), +): Environment { + Production( + envCode = "tidepool", + auth = AuthenticationServers.Production, + ), Integration( - "https://external.integration.tidepool.org", - "integration", - AuthenticationServers.External + envCode = "integration", + auth = AuthenticationServers.External + ), + Dev1( + envCode = "dev", + auth = AuthenticationServers.Development, ), - Dev1("https://dev1.dev.tidepool.org", "dev", AuthenticationServers.Development), - Qa1("https://qa1.development.tidepool.org", "qa1", AuthenticationServers.QA), - Qa2("https://qa2.development.tidepool.org", "qa2", AuthenticationServers.QA); + Qa1( + envCode = "qa1", + auth = AuthenticationServers.QA, + ), + Qa2( + envCode = "qa2", + auth = AuthenticationServers.QA, + ); +} + +internal fun Environment.toInternal() = when (this) { + Environments.Production -> EnvironmentInternal.Production + Environments.Integration -> EnvironmentInternal.Integration + Environments.Dev1 -> EnvironmentInternal.Dev1 + Environments.Qa1 -> EnvironmentInternal.Qa1 + Environments.Qa2 -> EnvironmentInternal.Qa2 - override val url: URL = URL(url) -} \ No newline at end of file + else -> EnvironmentInternal.Qa1 // TODO +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/Paginator.kt b/lib/src/main/kotlin/org/tidepool/sdk/Paginator.kt new file mode 100644 index 0000000..a5b607f --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/Paginator.kt @@ -0,0 +1,44 @@ +package org.tidepool.sdk + +import kotlinx.coroutines.flow.Flow + +abstract class Paginator( + protected val onLoadPageSuccess: suspend (result: Item, isEndReached: Boolean) -> Unit, + protected val onLoadPageFailure: suspend (result: Throwable) -> Unit, +) { + + /** + * Flow of accumulated items from all pagination requests. + * Emits a new list each time new items are loaded. + * The flow filters out null values and only emits when items are available. + */ + abstract val items: Flow> + + /** + * Loads the next batch of items from the paginated source. + * + * This method: + * - Checks if a request is already in progress or if the end has been reached + * - Makes a request using the current pagination key + * - Updates the current key for the next request + * - Appends new items to the existing list (or replaces if resetting) + * - Calls the appropriate success/failure callbacks + * + * The method is safe to call multiple times and will ignore concurrent calls. + */ + abstract suspend fun loadNextItems() + + /** + * Resets the paginator to its initial state. + * + * This method: + * - Resets the current key to the initial key + * - Clears the end-reached flag + * - Sets the reset flag so the next load will replace items instead of appending + * - Optionally clears the current items list immediately + * + * Call this method when you need to start pagination from the beginning, + * such as when implementing pull-to-refresh functionality. + */ + abstract fun reset(clearItems: Boolean = false) +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/PaginatorImpl.kt b/lib/src/main/kotlin/org/tidepool/sdk/PaginatorImpl.kt new file mode 100644 index 0000000..cdf77c8 --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/PaginatorImpl.kt @@ -0,0 +1,86 @@ +package org.tidepool.sdk + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update +import kotlin.collections.orEmpty +import kotlin.collections.plus + +/** + * A generic pagination utility that handles paginated API requests with automatic state management. + * + * This class provides a reactive way to load paginated data using Kotlin coroutines and flows. + * It manages the pagination state, handles loading requests, and exposes the accumulated items + * through a Flow for reactive UI updates. + * + * @param Key The type used for pagination keys (e.g., String, Int, custom pagination token) + * @param Item The type of individual items returned by each pagination request + * @property initialKey The starting key for pagination requests + * @property onRequest Suspend function that performs the actual API request for a given key + * @property getNextKey Suspend function that extracts the next pagination key from the response + * @property onSuccess Suspend function called when a request succeeds + * @property onFailure Suspend function called when a request fails + * @property endReached Function that determines if pagination has reached the end + */ +internal class PaginatorImpl( + private val initialKey: Key, + private val onRequest: suspend (nextKey: Key) -> Result, + private val getNextKey: suspend (result: Item, currentKey: Key) -> Key, + onSuccess: suspend (result: Item, isEndReached: Boolean) -> Unit, + onFailure: suspend (result: Throwable) -> Unit, + private val endReached: (result: Item, currentKey: Key) -> Boolean, +): Paginator(onSuccess, onFailure) { + + /** Internal state flow holding the accumulated list of items */ + private val _items = MutableStateFlow?>(null) + + /** Current pagination key used for the next request */ + private var currentKey = initialKey + + /** Flag to prevent concurrent requests */ + private var isMakingRequest = false + + /** Flag indicating if pagination has reached the end */ + private var isEndReached = false + + /** Flag indicating if the paginator is being reset */ + private var isReseting = false + + override val items: Flow> = _items.filterNotNull() + + override suspend fun loadNextItems() { + println("Paginator: loadNextItems() called with key: $currentKey, $isMakingRequest $isEndReached") + if (isMakingRequest || isEndReached) { + return + } + + isMakingRequest = true + + val result = onRequest(currentKey) + isMakingRequest = false + + result.onFailure { + onLoadPageFailure(it) + }.onSuccess { item -> + currentKey = getNextKey(item, currentKey) + isEndReached = endReached(item, currentKey) + onLoadPageSuccess(item, isEndReached) + _items.update { list -> + if (isReseting) { + listOf(item) + } else { + list.orEmpty() + item + } + } + isReseting = false + } + } + + override fun reset(clearItems: Boolean) { + if (clearItems) _items.update { emptyList() } + currentKey = initialKey + isEndReached = false + isReseting = true + } +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/ResultExt.kt b/lib/src/main/kotlin/org/tidepool/sdk/ResultExt.kt new file mode 100644 index 0000000..9bd36bb --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/ResultExt.kt @@ -0,0 +1,14 @@ +package org.tidepool.sdk + +internal fun Result>.mapList(block: (T) -> R): Result> = + map { list -> list.map { block(it) } } + +internal fun Result>.filterList(block: (T) -> Boolean): Result> = + map { list -> list.filter { block(it) } } + +internal suspend fun Result.flatMap( + block: suspend (T) -> Result +) = fold( + onSuccess = { block(it) }, + onFailure = { Result.failure(it) }, +) \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/TidepoolSDK.kt b/lib/src/main/kotlin/org/tidepool/sdk/TidepoolSDK.kt new file mode 100644 index 0000000..399764d --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/TidepoolSDK.kt @@ -0,0 +1,47 @@ +package org.tidepool.sdk + +import org.koin.core.Koin +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.tidepool.sdk.di.dataModule +import org.tidepool.sdk.di.domainModule +import org.tidepool.sdk.service.ConfirmationService +import org.tidepool.sdk.service.DataService +import org.tidepool.sdk.service.MetadataService +import org.tidepool.sdk.service.UserService +import java.time.Instant + +interface TokenProvider { + suspend fun getToken(): String +} + +class TidepoolSDK( + environment: Environment, + private val tokenProvider: TokenProvider, +) { + + // Internal DI container - not exposed + private val koin: Koin by lazy { + startKoin { + modules( + // Environment module + module { + single { environment.toInternal() } + single { tokenProvider } + }, + domainModule, + dataModule, + ) + }.koin + } + + val confirmations: ConfirmationService by lazy { koin.get() } + val data: DataService by lazy { koin.get() } + val metadata: MetadataService by lazy { koin.get() } + val users: UserService by lazy { koin.get() } + + public fun shutdown() { + stopKoin() + } +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/di/DomainModule.kt b/lib/src/main/kotlin/org/tidepool/sdk/di/DomainModule.kt new file mode 100644 index 0000000..7fd65c5 --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/di/DomainModule.kt @@ -0,0 +1,15 @@ +package org.tidepool.sdk.di + +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module +import org.tidepool.sdk.service.ConfirmationService +import org.tidepool.sdk.service.DataService +import org.tidepool.sdk.service.MetadataService +import org.tidepool.sdk.service.UserService + +val domainModule = module { + singleOf(::ConfirmationService) + singleOf(::DataService) + singleOf(::MetadataService) + singleOf(::UserService) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/Association.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/Association.kt index 9780df9..282d8d7 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/Association.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/Association.kt @@ -3,7 +3,6 @@ package org.tidepool.sdk.model import kotlinx.serialization.Serializable import kotlin.reflect.KClass -@Serializable public data class Association( val type: AssociationType?, val id: String?, @@ -11,7 +10,6 @@ public data class Association( val reason: String? ) { - @Serializable enum class AssociationType(val subclassType: KClass) { blob(Association::class), datum(Association::class), diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/BloodGlucose.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/BloodGlucose.kt index 9ead05b..1fdf997 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/BloodGlucose.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/BloodGlucose.kt @@ -1,22 +1,17 @@ package org.tidepool.sdk.model -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import org.tidepool.sdk.model.BloodGlucose.GlucoseReading -import org.tidepool.sdk.model.BloodGlucose.Units.milligramsPerDeciliter -import org.tidepool.sdk.model.BloodGlucose.Units.millimolesPerLiter +import org.tidepool.sdk.model.BloodGlucose.Units.MilligramsPerDeciliter +import org.tidepool.sdk.model.BloodGlucose.Units.MillimolesPerLiter import kotlin.math.roundToInt import kotlin.time.Duration public class BloodGlucose { - @Serializable public enum class Units(private val value: Double, val shorthand: String) { - @SerialName("mg/dL") - milligramsPerDeciliter(18.018, "mg/dL"), + MilligramsPerDeciliter(18.018, "mg/dL"), - @SerialName("mmol/L") - millimolesPerLiter(1.0, "mmol/L"); + MillimolesPerLiter(1.0, "mmol/L"); fun convert(amount: Double, units: Units): Double { if (this == units) { @@ -26,7 +21,6 @@ public class BloodGlucose { } } - @Serializable public class GlucoseReading(val amount: Double, val units: Units) : Comparable { public fun inUnit(newUnit: Units): Double { @@ -51,7 +45,7 @@ public class BloodGlucose { } override fun hashCode(): Int { - return inUnit(milligramsPerDeciliter).hashCode() + return inUnit(MilligramsPerDeciliter).hashCode() } override fun compareTo(other: GlucoseReading): Int { @@ -64,38 +58,36 @@ public class BloodGlucose { fun toString(unit: Units): String { return when (unit) { - millimolesPerLiter -> inUnit(unit).roundMillimolesPerLiter().toString() - milligramsPerDeciliter -> inUnit(unit).roundToInt().toString() + MillimolesPerLiter -> inUnit(unit).roundMillimolesPerLiter().toString() + MilligramsPerDeciliter -> inUnit(unit).roundToInt().toString() } } fun toSignString(unit: Units): String { return when (unit) { - millimolesPerLiter -> inUnit(unit).let { + MillimolesPerLiter -> inUnit(unit).let { if (it == 0.0) "0" else "%+.1f".format( it.roundMillimolesPerLiter() ) } - milligramsPerDeciliter -> inUnit(unit).let { amount -> + MilligramsPerDeciliter -> inUnit(unit).let { amount -> amount.roundToInt().let { if (it == 0) "0" else "%+d".format(it) } } } } } - @Serializable enum class Trend { - constant, - slowFall, - slowRise, - moderateFall, - moderateRise, - rapidFall, - rapidRise + Constant, + SlowFall, + SlowRise, + ModerateFall, + ModerateRise, + RapidFall, + RapidRise, } - @Serializable public data class Target( val target: Double?, val range: Double?, @@ -103,7 +95,6 @@ public class BloodGlucose { val high: Double? ) - @Serializable public data class StartTarget( val start: Duration?, val target: Double?, @@ -114,25 +105,21 @@ public class BloodGlucose { companion object { - private fun Units.valueRange(): ClosedRange { - return when (this) { - milligramsPerDeciliter -> 0.0..1000.0 - millimolesPerLiter -> 0.0..55.0 - } + private fun Units.valueRange() = when (this) { + MilligramsPerDeciliter -> 0.0..1000.0 + MillimolesPerLiter -> 0.0..55.0 } - fun clamp(value: Double, units: Units): Double { - return value.coerceIn(units.valueRange()) - } + fun clamp(value: Double, units: Units) = value.coerceIn(units.valueRange()) } } -public val Int.mgdl: GlucoseReading get() = GlucoseReading(toDouble(), milligramsPerDeciliter) -public val Long.mgdl: GlucoseReading get() = GlucoseReading(toDouble(), milligramsPerDeciliter) -public val Float.mgdl: GlucoseReading get() = GlucoseReading(toDouble(), milligramsPerDeciliter) -public val Double.mgdl: GlucoseReading get() = GlucoseReading(this, milligramsPerDeciliter) +public val Int.mgdl: GlucoseReading get() = GlucoseReading(toDouble(), MilligramsPerDeciliter) +public val Long.mgdl: GlucoseReading get() = GlucoseReading(toDouble(), MilligramsPerDeciliter) +public val Float.mgdl: GlucoseReading get() = GlucoseReading(toDouble(), MilligramsPerDeciliter) +public val Double.mgdl: GlucoseReading get() = GlucoseReading(this, MilligramsPerDeciliter) -public val Int.mmoll: GlucoseReading get() = GlucoseReading(toDouble(), millimolesPerLiter) -public val Long.mmoll: GlucoseReading get() = GlucoseReading(toDouble(), millimolesPerLiter) -public val Float.mmoll: GlucoseReading get() = GlucoseReading(toDouble(), millimolesPerLiter) -public val Double.mmoll: GlucoseReading get() = GlucoseReading(this, millimolesPerLiter) +public val Int.mmoll: GlucoseReading get() = GlucoseReading(toDouble(), MillimolesPerLiter) +public val Long.mmoll: GlucoseReading get() = GlucoseReading(toDouble(), MillimolesPerLiter) +public val Float.mmoll: GlucoseReading get() = GlucoseReading(toDouble(), MillimolesPerLiter) +public val Double.mmoll: GlucoseReading get() = GlucoseReading(this, MillimolesPerLiter) diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/auth/Realm.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/auth/Realm.kt index 905d6b9..7141bcc 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/auth/Realm.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/auth/Realm.kt @@ -1,14 +1,13 @@ package org.tidepool.sdk.model.auth; -@Suppress("EnumEntryName") enum class Realm { - - dev1, - qa1, - qa2, - qa3, - qa4, - qa5, - integration, - tidepool + Dev1, + Qa1, + Qa2, + Qa3, + Qa4, + Qa5, + Integration, + Tidepool, + ; } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Confirmation.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Confirmation.kt index 8e0a308..880b86f 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Confirmation.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Confirmation.kt @@ -1,29 +1,65 @@ package org.tidepool.sdk.model.confirmations -import kotlinx.serialization.Serializable -import kotlinx.serialization.Contextual import kotlinx.serialization.json.JsonObject +import org.tidepool.sdk.dto.confirmation.ConfirmationDto import org.tidepool.sdk.model.metadata.Profile +import org.tidepool.sdk.model.metadata.toDomain +import org.tidepool.sdk.model.metadata.toDto import java.time.Instant -@Serializable data class Confirmation( val key: String = "", - val type: ConfirmationType = ConfirmationType.signup_confirmation, - val status: ConfirmationStatus = ConfirmationStatus.pending, + val type: ConfirmationType = ConfirmationType.SignupConfirmation, + val status: ConfirmationStatus = ConfirmationStatus.Pending, val email: String = "", val creatorId: String = "", - @Contextual val created: Instant = Instant.now(), - @Contextual val modified: Instant? = null, + val created: Instant = Instant.now(), + val modified: Instant? = null, val creator: Creator? = null, val context: JsonObject? = null, // no context for context :3 val restrictions: Restrictions? = null, - @Contextual val expiresAt: Instant? = null, + val expiresAt: Instant? = null, ) { - @Serializable data class Creator( val userId: String = "", val profile: Profile = Profile(), ) } + +internal fun Confirmation.toDto(): ConfirmationDto = ConfirmationDto( + key = key, + type = type.toDto(), + status = status.toDto(), + email = email, + creatorId = creatorId, + created = created, + modified = modified, + creator = creator?.toDto(), + context = context, + restrictions = restrictions?.toDto(), + expiresAt = expiresAt +) + +internal fun ConfirmationDto.toDomain(): Confirmation = Confirmation( + key = key, + type = type.toDomain(), + status = status.toDomain(), + email = email, + creatorId = creatorId, + created = created, + modified = modified, + creator = creator?.toDomain(), + context = context, + restrictions = restrictions?.toDomain(), + expiresAt = expiresAt +) +internal fun Confirmation.Creator.toDto() = ConfirmationDto.CreatorDto( + userId = userId, + profile = profile.toDto() +) + +internal fun ConfirmationDto.CreatorDto.toDomain() = Confirmation.Creator( + userId = userId, + profile = profile.toDomain() +) \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationStatus.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationStatus.kt index da37443..14bd335 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationStatus.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationStatus.kt @@ -1,11 +1,29 @@ package org.tidepool.sdk.model.confirmations -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.confirmation.ConfirmationStatusDto +import org.tidepool.sdk.model.confirmations.ConfirmationStatus.Canceled +import org.tidepool.sdk.model.confirmations.ConfirmationStatus.Completed +import org.tidepool.sdk.model.confirmations.ConfirmationStatus.Declined +import org.tidepool.sdk.model.confirmations.ConfirmationStatus.Pending -@Serializable enum class ConfirmationStatus { - pending, - completed, - canceled, - declined + Pending, + Completed, + Canceled, + Declined, + ; +} + +internal fun ConfirmationStatus.toDto() = when (this) { + Pending -> ConfirmationStatusDto.Pending + Completed -> ConfirmationStatusDto.Completed + Canceled -> ConfirmationStatusDto.Canceled + Declined -> ConfirmationStatusDto.Declined +} + +internal fun ConfirmationStatusDto.toDomain() = when (this) { + ConfirmationStatusDto.Pending -> Pending + ConfirmationStatusDto.Completed -> Completed + ConfirmationStatusDto.Canceled -> Canceled + ConfirmationStatusDto.Declined -> Declined } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationType.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationType.kt index 80cbd88..44a9d3c 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationType.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/ConfirmationType.kt @@ -1,11 +1,26 @@ package org.tidepool.sdk.model.confirmations -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.confirmation.ConfirmationTypeDto -@Serializable enum class ConfirmationType { - password_reset, - careteam_invitation, - signup_confirmation, - no_account + + PasswordReset, + CareteamInvitation, + SignupConfirmation, + NoAccount, + ; +} + +internal fun ConfirmationType.toDto() = when (this) { + ConfirmationType.PasswordReset -> ConfirmationTypeDto.PasswordReset + ConfirmationType.CareteamInvitation -> ConfirmationTypeDto.CareteamInvitation + ConfirmationType.SignupConfirmation -> ConfirmationTypeDto.SignupConfirmation + ConfirmationType.NoAccount -> ConfirmationTypeDto.NoAccount +} + +internal fun ConfirmationTypeDto.toDomain() = when (this) { + ConfirmationTypeDto.PasswordReset -> ConfirmationType.PasswordReset + ConfirmationTypeDto.CareteamInvitation -> ConfirmationType.CareteamInvitation + ConfirmationTypeDto.SignupConfirmation -> ConfirmationType.SignupConfirmation + ConfirmationTypeDto.NoAccount -> ConfirmationType.NoAccount } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Restrictions.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Restrictions.kt index b077267..96c2431 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Restrictions.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/confirmations/Restrictions.kt @@ -1,9 +1,18 @@ package org.tidepool.sdk.model.confirmations -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.confirmation.RestrictionsDto -@Serializable data class Restrictions( val canAccept: Boolean, val requiredIdp: String? +) + +internal fun Restrictions.toDto() = RestrictionsDto( + canAccept = canAccept, + requiredIdp = requiredIdp, +) + +internal fun RestrictionsDto.toDomain() = Restrictions( + canAccept = canAccept, + requiredIdp = requiredIdp, ) \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/BasalAutomatedData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/BasalAutomatedData.kt index 6601561..b21a119 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/BasalAutomatedData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/BasalAutomatedData.kt @@ -1,27 +1,41 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.data.BasalAutomatedDataDto +import org.tidepool.sdk.dto.data.BasalAutomatedDataDto.DeliveryTypeDto // TODO: Finish implementing automated.v1 -@Serializable data class BasalAutomatedData( val deliveryType: DeliveryType, val duration: Int, val expectedDuration: Int? = null, val rate: Double = -1.0, val scheduleName: String? = null, -) : BaseData(DataType.basal) { +) : BaseData(DataType.Basal) { val insulinFormulation: Nothing get() = TODO("schema \"formulation.v1\" not implemented") val suppressed: Nothing get() = TODO("schema \"scheduled.v1\" not implemented") - @Serializable enum class DeliveryType { - automated, - scheduled, - suspend, - temp + Automated, + Scheduled, + Suspend, + Temp, } +} + +internal fun BasalAutomatedDataDto.toDomain() = BasalAutomatedData( + deliveryType = deliveryType.toDomain(), + duration = duration, + expectedDuration = expectedDuration, + rate = rate, + scheduleName = scheduleName, +) + +internal fun DeliveryTypeDto.toDomain(): BasalAutomatedData.DeliveryType = when (this) { + DeliveryTypeDto.Automated -> BasalAutomatedData.DeliveryType.Automated + DeliveryTypeDto.Scheduled -> BasalAutomatedData.DeliveryType.Scheduled + DeliveryTypeDto.Suspend -> BasalAutomatedData.DeliveryType.Suspend + DeliveryTypeDto.Temp -> BasalAutomatedData.DeliveryType.Temp } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/BaseData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/BaseData.kt index 016f097..95d8e60 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/BaseData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/BaseData.kt @@ -1,9 +1,7 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Contextual import org.tidepool.sdk.deserialization.ResultType +import org.tidepool.sdk.dto.data.* import org.tidepool.sdk.model.Association import java.time.Instant import java.util.TimeZone @@ -11,46 +9,51 @@ import kotlin.reflect.KClass import kotlin.time.Duration // TODO: finish implementing base.v1 -@Serializable sealed class BaseData( - val type: DataType = DataType.alert, - @Contextual val time: Instant? = null, + val type: DataType = DataType.Alert, + val time: Instant? = null, val annotations: Array>? = null, val associations: Array? = null, - @Contextual val clockDriftOffset: Duration? = null, - @Contextual val conversionOffset: Duration? = null, + val clockDriftOffset: Duration? = null, + val conversionOffset: Duration? = null, val dataSetId: String? = null, val deviceTime: String? = null, val id: String? = null, val notes: Array? = null, - @Contextual val timeZone: TimeZone? = null, - @Contextual val timeZoneOffset: Duration? = null + val timeZone: TimeZone? = null, + val timeZoneOffset: Duration? = null ) { val location: Nothing get() = TODO("schema \"\" not implemented") - @Serializable - enum class DataType(override val subclassType: KClass) : ResultType { - alert(BaseData::class), - basal(BasalAutomatedData::class), - bloodKetone(BaseData::class), - bolus(BolusData::class), - - @SerialName("wizard") - calculator(BaseData::class), - cbg(ContinuousGlucoseData::class), - cgmSettings(BaseData::class), - controllerSettings(BaseData::class), - controllerStatus(BaseData::class), - deviceEvent(BaseData::class), - dosingDecision(DosingDecisionData::class), - food(FoodData::class), - insulin(InsulinData::class), - physicalActivity(BaseData::class), - pumpSettings(BaseData::class), - pumpStatus(BaseData::class), - reportedState(BaseData::class), - smbg(BaseData::class) + enum class DataType() { + Alert, + Basal, + BloodKetone, + Bolus, + Calculator, + Cbg, + CgmSettings, + ControllerSettings, + ControllerStatus, + DeviceEvent, + DosingDecision, + Food, + Insulin, + PhysicalActivity, + PumpSettings, + PumpStatus, + ReportedState, + Smbg } -} \ No newline at end of file +} + +internal fun BaseDataDto.toDomain() = when (this) { + is BasalAutomatedDataDto -> toDomain() + is BolusDataDto -> toDomain() + is ContinuousGlucoseDataDto -> toDomain() + is DosingDecisionDataDto -> toDomain() + is FoodDataDto -> toDomain() + is InsulinDataDto -> toDomain() +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/BolusData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/BolusData.kt index f6950ac..db5fdaf 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/BolusData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/BolusData.kt @@ -1,34 +1,49 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.data.BolusDataDto +import org.tidepool.sdk.dto.data.BolusSubtypeDto +import org.tidepool.sdk.dto.data.DeliveryContextDto // schema bolus.v1 // line 2330 -@Serializable data class BolusData( - val subType: BolusSubtype = BolusSubtype.normal, + val subType: BolusSubtype = BolusSubtype.Normal, val deliveryContext: DeliveryContext, -) : BaseData(DataType.bolus) { +) : BaseData(DataType.Bolus) { val insulinFormulation: Nothing get() = TODO("schema \"formulation.v1\" not implemented") } -@Serializable enum class BolusSubtype { - automated, - - @SerialName("dual/square") - dual_square, - normal, - square, + Automated, + DualSquare, + Normal, + Square, } -@Serializable enum class DeliveryContext { - device, - algorithm, - remote, - undetermined + Device, + Algorithm, + Remote, + Undetermined +} + +internal fun BolusDataDto.toDomain() = BolusData( + subType = subType.toDomain(), + deliveryContext = deliveryContext.toDomain() +) + +internal fun DeliveryContextDto.toDomain(): DeliveryContext = when (this) { + DeliveryContextDto.Device -> DeliveryContext.Device + DeliveryContextDto.Algorithm -> DeliveryContext.Algorithm + DeliveryContextDto.Remote -> DeliveryContext.Remote + DeliveryContextDto.Undetermined -> DeliveryContext.Undetermined +} + +internal fun BolusSubtypeDto.toDomain(): BolusSubtype = when (this) { + BolusSubtypeDto.Automated -> BolusSubtype.Automated + BolusSubtypeDto.DualSquare -> BolusSubtype.DualSquare + BolusSubtypeDto.Normal -> BolusSubtype.Normal + BolusSubtypeDto.Square -> BolusSubtype.Square } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/ContinuousGlucoseData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/ContinuousGlucoseData.kt index c096bbc..0f3896f 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/ContinuousGlucoseData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/ContinuousGlucoseData.kt @@ -1,22 +1,24 @@ package org.tidepool.sdk.model.data import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.BloodGlucoseDto.TrendDto +import org.tidepool.sdk.dto.BloodGlucoseDto.UnitsDto +import org.tidepool.sdk.dto.data.ContinuousGlucoseDataDto import org.tidepool.sdk.model.BloodGlucose import org.tidepool.sdk.model.BloodGlucose.GlucoseReading -@Serializable public data class ContinuousGlucoseData( val value: Double? = null, val units: BloodGlucose.Units? = null, val trend: BloodGlucose.Trend? = null, val trendRate: Double? = null -) : BaseData(type = BaseData.DataType.cbg) { +) : BaseData(type = DataType.Cbg) { - public constructor( - reading: GlucoseReading?, - trend: BloodGlucose.Trend?, - trendRate: Double? - ) : this(reading?.amount, reading?.units, trend, trendRate) + // public constructor( + // reading: GlucoseReading?, + // trend: BloodGlucose.Trend?, + // trendRate: Double? + // ) : this(reading?.amount, reading?.units, trend, trendRate) val reading: GlucoseReading? by lazy { value?.let { value -> @@ -31,4 +33,23 @@ public data class ContinuousGlucoseData( trend: BloodGlucose.Trend? = this.trend, trendRate: Double? = this.trendRate ) = copy(reading?.amount, reading?.units, trend, trendRate) -} \ No newline at end of file +} + +internal fun ContinuousGlucoseDataDto.toDomain() = ContinuousGlucoseData( + value = this.value, + units = when (units) { + UnitsDto.MilligramsPerDeciliter -> BloodGlucose.Units.MilligramsPerDeciliter + UnitsDto.MillimolesPerLiter -> BloodGlucose.Units.MillimolesPerLiter + null -> null + }, + trend = when (trend) { + TrendDto.Constant -> BloodGlucose.Trend.Constant + TrendDto.SlowFall -> BloodGlucose.Trend.SlowFall + TrendDto.SlowRise -> BloodGlucose.Trend.SlowRise + TrendDto.ModerateFall -> BloodGlucose.Trend.ModerateFall + TrendDto.ModerateRise -> BloodGlucose.Trend.ModerateRise + TrendDto.RapidFall -> BloodGlucose.Trend.RapidFall + TrendDto.RapidRise -> BloodGlucose.Trend.RapidRise + null -> null + } +) diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/DataType.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/DataType.kt new file mode 100644 index 0000000..ecfb8ea --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/DataType.kt @@ -0,0 +1,67 @@ +package org.tidepool.sdk.model.data + +import org.tidepool.sdk.dto.data.BaseDataDto.DataTypeDto + +enum class DataType { + Alert, + Basal, + BloodKetone, + Bolus, + Calculator, + Cbg, + CgmSettings, + ControllerSettings, + ControllerStatus, + DeviceEvent, + DosingDecision, + Food, + Insulin, + PhysicalActivity, + PumpSettings, + PumpStatus, + ReportedState, + Smbg, + ; +} + +internal fun DataType.toDto(): DataTypeDto = when (this) { + DataType.Alert -> DataTypeDto.Alert + DataType.Basal -> DataTypeDto.Basal + DataType.BloodKetone -> DataTypeDto.BloodKetone + DataType.Bolus -> DataTypeDto.Bolus + DataType.Calculator -> DataTypeDto.Calculator + DataType.Cbg -> DataTypeDto.Cbg + DataType.CgmSettings -> DataTypeDto.CgmSettings + DataType.ControllerSettings -> DataTypeDto.ControllerSettings + DataType.ControllerStatus -> DataTypeDto.ControllerStatus + DataType.DeviceEvent -> DataTypeDto.DeviceEvent + DataType.DosingDecision -> DataTypeDto.DosingDecision + DataType.Food -> DataTypeDto.Food + DataType.Insulin -> DataTypeDto.Insulin + DataType.PhysicalActivity -> DataTypeDto.PhysicalActivity + DataType.PumpSettings -> DataTypeDto.PumpSettings + DataType.PumpStatus -> DataTypeDto.PumpStatus + DataType.ReportedState -> DataTypeDto.ReportedState + DataType.Smbg -> DataTypeDto.Smbg +} + +internal fun DataTypeDto.toDomain(): DataType = when (this) { + DataTypeDto.Alert -> DataType.Alert + DataTypeDto.Basal -> DataType.Basal + DataTypeDto.BloodKetone -> DataType.BloodKetone + DataTypeDto.Bolus -> DataType.Bolus + DataTypeDto.Calculator -> DataType.Calculator + DataTypeDto.Cbg -> DataType.Cbg + DataTypeDto.CgmSettings -> DataType.CgmSettings + DataTypeDto.ControllerSettings -> DataType.ControllerSettings + DataTypeDto.ControllerStatus -> DataType.ControllerStatus + DataTypeDto.DeviceEvent -> DataType.DeviceEvent + DataTypeDto.DosingDecision -> DataType.DosingDecision + DataTypeDto.Food -> DataType.Food + DataTypeDto.Insulin -> DataType.Insulin + DataTypeDto.PhysicalActivity -> DataType.PhysicalActivity + DataTypeDto.PumpSettings -> DataType.PumpSettings + DataTypeDto.PumpStatus -> DataType.PumpStatus + DataTypeDto.ReportedState -> DataType.ReportedState + DataTypeDto.Smbg -> DataType.Smbg +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/DosingDecisionData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/DosingDecisionData.kt index cf24705..df122f7 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/DosingDecisionData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/DosingDecisionData.kt @@ -1,13 +1,11 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Contextual +import org.tidepool.sdk.dto.BloodGlucoseDto +import org.tidepool.sdk.dto.data.DosingDecisionDataDto import org.tidepool.sdk.model.BloodGlucose import java.time.Instant // TODO: finish implementing dosingdecision.v1 -@Serializable data class DosingDecisionData( val reason: String, val carbsOnBoard: CarbsOnBoard? = null, @@ -17,7 +15,7 @@ data class DosingDecisionData( val requestedBolus: RequestedBolus? = null, val scheduleTimeZoneOffset: Int? = null, val units: Units = Units(), -) : BaseData(DataType.dosingDecision) { +) : BaseData(DataType.DosingDecision) { val originalFood: Nothing get() = TODO("backing object not implemented") @@ -36,54 +34,99 @@ data class DosingDecisionData( val errors: Nothing get() = TODO("schema \"issue.v1\" not implemented") - @Serializable data class CarbsOnBoard( - @Contextual val time: Instant? = null, + val time: Instant? = null, val amount: Double = -1.0, ) - @Serializable data class InsulinOnBoard( - @Contextual val time: Instant? = null, + val time: Instant? = null, val amount: Double = -1.0, ) - @Serializable data class RecommendedBasal( val rate: Double = -1.0, val duration: Double? = null, ) - @Serializable data class RecommendedBolus( val amount: Double = -1.0, ) - @Serializable data class RequestedBolus( val amount: Double = -1.0, ) - @Serializable data class Units( - val bg: BloodGlucose.Units = BloodGlucose.Units.milligramsPerDeciliter, + val bg: BloodGlucose.Units = BloodGlucose.Units.MilligramsPerDeciliter, val carb: Carb = Carb.Exchanges, val insulin: Insulin = Insulin.Units, ) { - @Serializable enum class Carb { - - @SerialName("exchanges") Exchanges, - - @SerialName("grams") - Grams + Grams, + ; } - @Serializable enum class Insulin { Units, } } +} + +internal fun DosingDecisionDataDto.toDomain() = DosingDecisionData( + reason = reason, + carbsOnBoard = carbsOnBoard?.toDomain(), + insulinOnBoard = insulinOnBoard?.toDomain(), + recommendedBasal = recommendedBasal?.toDomain(), + recommendedBolus = recommendedBolus?.toDomain(), + requestedBolus = requestedBolus?.toDomain(), + scheduleTimeZoneOffset = scheduleTimeZoneOffset, + units = units.toDomain(), +) + +internal fun DosingDecisionDataDto.CarbsOnBoardDto.toDomain() = DosingDecisionData.CarbsOnBoard( + time = time, + amount = amount, +) + +internal fun DosingDecisionDataDto.InsulinOnBoardDto.toDomain() = DosingDecisionData.InsulinOnBoard( + time = time, + amount = amount, +) + +internal fun DosingDecisionDataDto.RecommendedBasalDto.toDomain() = + DosingDecisionData.RecommendedBasal( + rate = rate, + duration = duration, +) + +internal fun DosingDecisionDataDto.RecommendedBolusDto.toDomain() = + DosingDecisionData.RecommendedBolus( + amount = amount, +) + +internal fun DosingDecisionDataDto.RequestedBolusDto.toDomain() = DosingDecisionData.RequestedBolus( + amount = amount, +) + +internal fun DosingDecisionDataDto.UnitsDto.toDomain() = DosingDecisionData.Units( + bg = bg.toDomain(), + carb = carb.toDomain(), + insulin = insulin.toDomain(), +) + +internal fun DosingDecisionDataDto.UnitsDto.InsulinDto.toDomain() = when (this) { + DosingDecisionDataDto.UnitsDto.InsulinDto.Units -> DosingDecisionData.Units.Insulin.Units +} + +internal fun DosingDecisionDataDto.UnitsDto.CarbDto.toDomain() = when (this) { + DosingDecisionDataDto.UnitsDto.CarbDto.Exchanges -> DosingDecisionData.Units.Carb.Exchanges + DosingDecisionDataDto.UnitsDto.CarbDto.Grams -> DosingDecisionData.Units.Carb.Grams +} + +internal fun BloodGlucoseDto.UnitsDto.toDomain() = when (this) { + BloodGlucoseDto.UnitsDto.MilligramsPerDeciliter -> BloodGlucose.Units.MilligramsPerDeciliter + BloodGlucoseDto.UnitsDto.MillimolesPerLiter -> BloodGlucose.Units.MillimolesPerLiter } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/FoodData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/FoodData.kt index 12c7e05..dfdfe73 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/FoodData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/FoodData.kt @@ -1,16 +1,15 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.data.FoodDataDto // schema food.v1 -@Serializable data class FoodData( val brand: String? = null, val code: String? = null, val meal: Meal? = null, val mealOther: String? = null, val name: String? = null, -) : BaseData(DataType.food) { +) : BaseData(DataType.Food) { val amount: Nothing get() = TODO("schema \"amount.v1\" not implemented") @@ -19,12 +18,27 @@ data class FoodData( val nutrition: Nothing get() = TODO("schema \"nutrition.v1\" not implemented") - @Serializable enum class Meal { - breakfast, - lunch, - dinner, - snack, - other, + Breakfast, + Lunch, + Dinner, + Snack, + Other, } +} + +internal fun FoodDataDto.toDomain(): FoodData = FoodData( + brand = brand, + code = code, + meal = meal?.toDomain(), + mealOther = mealOther, + name = name, +) + +internal fun FoodDataDto.MealDto.toDomain(): FoodData.Meal = when (this) { + FoodDataDto.MealDto.breakfast -> FoodData.Meal.Breakfast + FoodDataDto.MealDto.lunch -> FoodData.Meal.Lunch + FoodDataDto.MealDto.dinner -> FoodData.Meal.Dinner + FoodDataDto.MealDto.snack -> FoodData.Meal.Snack + FoodDataDto.MealDto.other -> FoodData.Meal.Other } \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/Insulin.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/Insulin.kt index 604aa95..759bb72 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/Insulin.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/Insulin.kt @@ -1,22 +1,33 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.data.DoseDto +import org.tidepool.sdk.dto.data.InsulinDto -@Serializable class Insulin { - @Serializable enum class Units { + Units } } // schema dose.v1 -@Serializable data class Dose( val units: Insulin.Units, val total: Double, val food: Double?, val correction: Double?, val active: Double?, -) \ No newline at end of file +) + +internal fun DoseDto.toDomain() = Dose( + units = units.toDomain(), + total = total, + food = food, + correction = correction, + active = active, +) + +internal fun InsulinDto.UnitsDto.toDomain() = when (this) { + InsulinDto.UnitsDto.Units -> Insulin.Units.Units +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/data/InsulinData.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/data/InsulinData.kt index 2454d67..5bdc764 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/data/InsulinData.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/data/InsulinData.kt @@ -1,14 +1,18 @@ package org.tidepool.sdk.model.data -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.data.InsulinDataDto // schema insulin.v1 -@Serializable data class InsulinData( val dose: Dose, val site: String? -) : BaseData(DataType.insulin) { +) : BaseData(DataType.Insulin) { val formulation: Nothing get() = TODO("schema \"formulation.v1\" not implemented") -} \ No newline at end of file +} + +internal fun InsulinDataDto.toDomain() = InsulinData( + dose = dose.toDomain(), + site = site, +) \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/Profile.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/Profile.kt index 8106574..c1965c0 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/Profile.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/Profile.kt @@ -1,8 +1,15 @@ package org.tidepool.sdk.model.metadata -import kotlinx.serialization.Serializable +import org.tidepool.sdk.dto.metadata.ProfileDto -@Serializable -open class Profile(val fullName: String? = null) { +data class Profile( + val fullName: String? = null, +) -} \ No newline at end of file +internal fun Profile.toDto() = ProfileDto( + fullName = fullName, +) + +internal fun ProfileDto.toDomain() = Profile( + fullName = fullName, +) \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/Permission.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/Permission.kt new file mode 100644 index 0000000..baef18e --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/Permission.kt @@ -0,0 +1,25 @@ +package org.tidepool.sdk.model.metadata.users + +import org.tidepool.sdk.dto.metadata.users.PermissionDto + +enum class Permission { + Custodian, + View, + Note, + Upload, + ; +} + +internal fun PermissionDto.toDomain() = when (this) { + PermissionDto.Custodian -> Permission.Custodian + PermissionDto.View -> Permission.View + PermissionDto.Note -> Permission.Note + PermissionDto.Upload -> Permission.Upload +} + +internal fun Permission.toDto() = when (this) { + Permission.Custodian -> PermissionDto.Custodian + Permission.View -> PermissionDto.View + Permission.Note -> PermissionDto.Note + Permission.Upload -> PermissionDto.Upload +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustUser.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustUser.kt index 604d028..2a92296 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustUser.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustUser.kt @@ -1,40 +1,141 @@ package org.tidepool.sdk.model.metadata.users -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject -import java.util.Collections -import java.util.EnumSet +import org.tidepool.sdk.dto.metadata.users.TrustUserDto +import org.tidepool.sdk.model.metadata.Profile +import java.time.Instant +import kotlin.collections.map -/** - * Marker class so that deserialization will work properly - */ -@Serializable -open class TrustUser : User() { +sealed class TrustUser( + override val emailVerified: Boolean = false, + override val emails: List? = null, + override val termsAccepted: Instant? = null, + override val userId: String = "", + override val username: String? = null, + override val roles: List? = null, + override val createdTime: Instant? = null, + override val createdUserId: String? = null, + override val modifiedTime: Instant? = null, + override val modifiedUserId: String? = null, + override val deletedTime: Instant? = null, + override val deletedUserId: String? = null, + override val profile: Profile? = null, +) : User( + emailVerified = emailVerified, + emails = emails, + termsAccepted = termsAccepted, + userId = userId, + username = username, + roles = roles, + createdTime = createdTime, + createdUserId = createdUserId, + modifiedTime = modifiedTime, + modifiedUserId = modifiedUserId, + deletedTime = deletedTime, + deletedUserId = deletedUserId, + profile = profile, +) { - @Serializable - enum class Permission { - custodian, - view, - note, - upload - } + data class TrustorUser( + val permissions: Set, + override val emailVerified: Boolean = false, + override val emails: List? = null, + override val termsAccepted: Instant? = null, + override val userId: String = "", + override val username: String? = null, + override val roles: List? = null, + override val createdTime: Instant? = null, + override val createdUserId: String? = null, + override val modifiedTime: Instant? = null, + override val modifiedUserId: String? = null, + override val deletedTime: Instant? = null, + override val deletedUserId: String? = null, + override val profile: Profile? = null, + ) : TrustUser( + emailVerified = emailVerified, + emails = emails, + termsAccepted = termsAccepted, + userId = userId, + username = username, + roles = roles, + createdTime = createdTime, + createdUserId = createdUserId, + modifiedTime = modifiedTime, + modifiedUserId = modifiedUserId, + deletedTime = deletedTime, + deletedUserId = deletedUserId, + profile = profile, + ) - @Serializable - data class JsonPermissions( - private val custodian: JsonObject? = null, - private val view: JsonObject? = null, - private val note: JsonObject? = null, - private val upload: JsonObject? = null - ) { - - val permissionsSet: Set by lazy { - setOfNotNull( - Permission.custodian.takeUnless { custodian == null }, - Permission.view.takeUnless { view == null }, - Permission.note.takeUnless { note == null }, - Permission.upload.takeUnless { upload == null }, - ) + data class TrusteeUser( + val permissions: Set, + override val emailVerified: Boolean = false, + override val emails: List? = null, + override val termsAccepted: Instant? = null, + override val userId: String = "", + override val username: String? = null, + override val roles: List? = null, + override val createdTime: Instant? = null, + override val createdUserId: String? = null, + override val modifiedTime: Instant? = null, + override val modifiedUserId: String? = null, + override val deletedTime: Instant? = null, + override val deletedUserId: String? = null, + override val profile: Profile? = null, + ) : TrustUser( + emailVerified = emailVerified, + emails = emails, + termsAccepted = termsAccepted, + userId = userId, + username = username, + roles = roles, + createdTime = createdTime, + createdUserId = createdUserId, + modifiedTime = modifiedTime, + modifiedUserId = modifiedUserId, + deletedTime = deletedTime, + deletedUserId = deletedUserId, + profile = profile, + ) +} + +internal fun TrustUserDto.toDomain(): TrustUser = when { + isTrustor -> TrustUser.TrustorUser( + permissions = permissions.map { it.toDomain() }.toSet(), + emailVerified = emailVerified, + emails = emails, + termsAccepted = termsAccepted, + userId = userId, + username = userName, + roles = roles, + createdTime = createdTime, + createdUserId = createdUserId, + modifiedTime = modifiedTime, + modifiedUserId = modifiedUserId, + deletedTime = deletedTime, + deletedUserId = deletedUserId, + profile = profile?.let { + Profile(it.fullName) + } + ) + + isTrustee -> TrustUser.TrusteeUser( + permissions = permissions.map { it.toDomain() }.toSet(), + emailVerified = emailVerified, + emails = emails, + termsAccepted = termsAccepted, + userId = userId, + username = userName, + roles = roles, + createdTime = createdTime, + createdUserId = createdUserId, + modifiedTime = modifiedTime, + modifiedUserId = modifiedUserId, + deletedTime = deletedTime, + deletedUserId = deletedUserId, + profile = profile?.let { + Profile(it.fullName) } - } + ) -} \ No newline at end of file + else -> throw IllegalStateException("TrustUserDto must be either trustor or trustee") +} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrusteeUser.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrusteeUser.kt deleted file mode 100644 index 79446dd..0000000 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrusteeUser.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.tidepool.sdk.model.metadata.users - -import kotlinx.serialization.Serializable - -@Serializable -data class TrusteeUser(val trusteePermissions: JsonPermissions = JsonPermissions()) : TrustUser() { - - val permissions by trusteePermissions::permissionsSet -} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustorUser.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustorUser.kt deleted file mode 100644 index c7d04e9..0000000 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/TrustorUser.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.tidepool.sdk.model.metadata.users - -import kotlinx.serialization.Serializable - -@Serializable -data class TrustorUser(private val trustorPermissions: JsonPermissions = JsonPermissions()) : - TrustUser() { - - val permissions by trustorPermissions::permissionsSet -} diff --git a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/User.kt b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/User.kt index 781c76c..f865c52 100644 --- a/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/User.kt +++ b/lib/src/main/kotlin/org/tidepool/sdk/model/metadata/users/User.kt @@ -1,23 +1,39 @@ package org.tidepool.sdk.model.metadata.users -import kotlinx.serialization.Serializable -import kotlinx.serialization.Contextual +import org.tidepool.sdk.dto.user.UserDto import org.tidepool.sdk.model.metadata.Profile import java.time.Instant -@Serializable open class User( - val emailVerified: Boolean = false, - val emails: List? = null, - @Contextual val termsAccepted: Instant? = null, - val userid: String = "", - val username: String? = null, - val roles: List? = null, - @Contextual val createdTime: Instant? = null, - val createdUserId: String? = null, - @Contextual val modifiedTime: Instant? = null, - val modifiedUserId: String? = null, - @Contextual val deletedTime: Instant? = null, - val deletedUserId: String? = null, - val profile: Profile? = null, + open val emailVerified: Boolean = false, + open val emails: List? = null, + open val termsAccepted: Instant? = null, + open val userId: String = "", + open val username: String? = null, + open val roles: List? = null, + open val createdTime: Instant? = null, + open val createdUserId: String? = null, + open val modifiedTime: Instant? = null, + open val modifiedUserId: String? = null, + open val deletedTime: Instant? = null, + open val deletedUserId: String? = null, + open val profile: Profile? = null, +) + +internal fun UserDto.toDomain() = User( + emailVerified = emailVerified, + emails = emails ?: emptyList(), + termsAccepted = termsAccepted, + userId = userId, + username = userName, + roles = roles ?: emptyList(), + createdTime = createdTime, + createdUserId = createdUserId, + modifiedTime = modifiedTime, + modifiedUserId = modifiedUserId, + deletedTime = deletedTime, + deletedUserId = deletedUserId, + profile = profile?.let { + Profile(it.fullName) + } ) \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/requests/Auth.kt b/lib/src/main/kotlin/org/tidepool/sdk/requests/Auth.kt deleted file mode 100644 index f0e94fc..0000000 --- a/lib/src/main/kotlin/org/tidepool/sdk/requests/Auth.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.tidepool.sdk.requests - -import org.tidepool.sdk.auth.TokenResponse -import org.tidepool.sdk.model.auth.Realm -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Query -import java.net.URI - -public interface Auth { - - @POST("/realms/{realm}/protocol/openid-connect/token") - suspend fun obtainToken( - @Path("realm") realm: Realm, - @Body grantType: TokenRequest - ): TokenResponse - - @GET("/realms/{realm}/protocol/openid-connect/auth?response_type=code") - suspend fun authorize( - @Path("realm") realm: Realm, - @Query("client_id") clinetId: String, - @Query("scope") scope: Array, - @Query("redirect_uri") redirectUri: URI, - @Query("login_hint") loginHint: String? = null, - @Query("kc_idp_hint") kcIdpHint: String? = null, - @Query("prompt") prompt: PromptType? = null - ) - - enum class PromptType { - none, - login - } - - enum class ScopeType { - openid, - email - } -} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/requests/Confirmations.kt b/lib/src/main/kotlin/org/tidepool/sdk/requests/Confirmations.kt deleted file mode 100644 index ffeb85f..0000000 --- a/lib/src/main/kotlin/org/tidepool/sdk/requests/Confirmations.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.tidepool.sdk.requests - -import org.tidepool.sdk.model.confirmations.Confirmation -import org.tidepool.sdk.model.confirmations.ConfirmationLookup -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.PUT -import retrofit2.http.Path - -interface Confirmations { - - @GET("/confirm/invite/{userId}") - suspend fun getPendingCareTeamInvitations( - @Header("X-Tidepool-Session-Token") sessionToken: String, - @Path("userId") userId: String - ): Response> - - @GET("/confirm/invitations/{userId}") - suspend fun getReceivedInvitations( - @Header("X-Tidepool-Session-Token") sessionToken: String, - @Path("userId") userId: String - ): Response> - - @PUT("/confirm/accept/invite/{userId}/{invitedBy}") - suspend fun acceptCareTeamInvite( - @Header("X-Tidepool-Session-Token") sessionToken: String, - @Path("userId") userId: String, - @Path("invitedBy") invitedBy: String, - @Body requestBody: ConfirmationLookup - ) - - @PUT("/confirm/dismiss/invite/{userId}/{invitedBy}") - suspend fun dismissInvite( - @Header("X-Tidepool-Session-Token") sessionToken: String, - @Path("userId") userId: String, - @Path("invitedBy") invitedBy: String, - @Body requestBody: ConfirmationLookup - ) -} - -suspend fun Confirmations.pendingCareTeamInvitations( - sessionToken: String, - userId: String -): Array { - return getPendingCareTeamInvitations(sessionToken, userId).body() ?: arrayOf() -} - -suspend fun Confirmations.receivedInvitations( - sessionToken: String, - userId: String -): Array { - return getReceivedInvitations(sessionToken, userId).body() ?: arrayOf() -} - -private suspend fun Confirmation.perform( - sessionToken: String, - userId: String, - func: suspend (String, String, String, ConfirmationLookup) -> Unit -) { - func(sessionToken, userId, creatorId, ConfirmationLookup(key)) -} - -suspend fun Confirmations.accept( - sessionToken: String, - userId: String, - confirmation: Confirmation -) { - confirmation.perform(sessionToken, userId, this::acceptCareTeamInvite) -} - -suspend fun Confirmations.dismiss( - sessionToken: String, - userId: String, - confirmation: Confirmation -) { - confirmation.perform(sessionToken, userId, this::dismissInvite) -} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/service/ConfirmationService.kt b/lib/src/main/kotlin/org/tidepool/sdk/service/ConfirmationService.kt new file mode 100644 index 0000000..45f1e56 --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/service/ConfirmationService.kt @@ -0,0 +1,55 @@ +package org.tidepool.sdk.service + +import org.tidepool.sdk.TokenProvider +import org.tidepool.sdk.dto.confirmation.ConfirmationDto +import org.tidepool.sdk.flatMap +import org.tidepool.sdk.mapList +import org.tidepool.sdk.model.confirmations.Confirmation +import org.tidepool.sdk.model.confirmations.toDomain +import org.tidepool.sdk.repository.ConfirmationRepository +import org.tidepool.sdk.repository.UserRepository + +class ConfirmationService internal constructor( + private val confirmationRepository: ConfirmationRepository, + private val tokenProvider: TokenProvider, + private val userRepository: UserRepository, +) { + + suspend fun getReceivedInvitations(): Result> = tokenProvider.getToken() + .let { token -> + userRepository.getCurrentUser(sessionToken = token).flatMap { user -> + confirmationRepository.getReceivedInvitations( + sessionToken = token, + userId = user.userId, + ) + } + }.mapList(ConfirmationDto::toDomain) + + suspend fun acceptConfirmation( + confirmationKey: String, + creatorId: String, + ): Result = tokenProvider.getToken().let { token -> + userRepository.getCurrentUser(sessionToken = token).flatMap { user -> + confirmationRepository.acceptConfirmation( + sessionToken = tokenProvider.getToken(), + userId = user.userId, + confirmationKey = confirmationKey, + creatorId = creatorId, + ) + } + } + + suspend fun dismissConfirmation( + confirmationKey: String, + creatorId: String, + ): Result = tokenProvider.getToken().let { token -> + userRepository.getCurrentUser(sessionToken = token).flatMap { user -> + confirmationRepository.dismissConfirmation( + sessionToken = tokenProvider.getToken(), + userId = user.userId, + confirmationKey = confirmationKey, + creatorId = creatorId, + ) + } + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/service/DataService.kt b/lib/src/main/kotlin/org/tidepool/sdk/service/DataService.kt new file mode 100644 index 0000000..bec7cc8 --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/service/DataService.kt @@ -0,0 +1,42 @@ +package org.tidepool.sdk.service + +import org.tidepool.sdk.TokenProvider +import org.tidepool.sdk.dto.data.BaseDataDto +import org.tidepool.sdk.mapList +import org.tidepool.sdk.model.data.BaseData +import org.tidepool.sdk.model.data.DataType +import org.tidepool.sdk.model.data.toDomain +import org.tidepool.sdk.model.data.toDto +import org.tidepool.sdk.repository.DataRepository +import java.time.Instant + +class DataService internal constructor( + private val dataRepository: DataRepository, + private val tokenProvider: TokenProvider, +) { + + suspend fun getDataForUser( + userId: String, + uploadId: String? = null, + deviceId: String? = null, + types: List = emptyList(), + startDate: Instant? = null, + endDate: Instant? = null, + latest: Boolean? = null, + dexcom: Boolean? = null, + carelink: Boolean? = null, + medtronic: Boolean? = null, + ): Result> = dataRepository.getDataForUser( + userId = userId, + uploadId = uploadId, + deviceId = deviceId, + types = types.map { it.toDto() }, + startDate = startDate, + endDate = endDate, + latest = latest, + dexcom = dexcom, + carelink = carelink, + medtronic = medtronic, + sessionToken = tokenProvider.getToken() + ).mapList(BaseDataDto::toDomain) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/service/MetadataService.kt b/lib/src/main/kotlin/org/tidepool/sdk/service/MetadataService.kt new file mode 100644 index 0000000..5ff6916 --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/service/MetadataService.kt @@ -0,0 +1,16 @@ +package org.tidepool.sdk.service + +import org.tidepool.sdk.TokenProvider +import org.tidepool.sdk.dto.metadata.users.TrustUserDto +import org.tidepool.sdk.mapList +import org.tidepool.sdk.model.metadata.users.toDomain +import org.tidepool.sdk.repository.MetadataRepository + +class MetadataService internal constructor( + private val repository: MetadataRepository, + private val tokenProvider: TokenProvider, +) { + + suspend fun getTrustUsers() = repository.getTrustUsers(tokenProvider.getToken()) + .mapList(TrustUserDto::toDomain) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/org/tidepool/sdk/service/UserService.kt b/lib/src/main/kotlin/org/tidepool/sdk/service/UserService.kt new file mode 100644 index 0000000..8b07ec2 --- /dev/null +++ b/lib/src/main/kotlin/org/tidepool/sdk/service/UserService.kt @@ -0,0 +1,24 @@ +package org.tidepool.sdk.service + +import org.tidepool.sdk.TokenProvider +import org.tidepool.sdk.dto.user.UserDto +import org.tidepool.sdk.model.metadata.users.User +import org.tidepool.sdk.model.metadata.users.toDomain +import org.tidepool.sdk.repository.UserRepository + +class UserService internal constructor( + private val repository: UserRepository, + private val tokenProvider: TokenProvider, +) { + + suspend fun getCurrentUser(): Result = repository.getCurrentUser( + sessionToken = tokenProvider.getToken(), + ).map(UserDto::toDomain) + + suspend fun getUser( + userId: String, + ): Result = repository.getUser( + sessionToken = tokenProvider.getToken(), + userId = userId, + ).map(UserDto::toDomain) +} \ No newline at end of file diff --git a/lib/src/test/kotlin/org/tidepool/sdk/TestClass.kt b/lib/src/test/kotlin/org/tidepool/sdk/TestClass.kt index 8f0c091..aceeda2 100644 --- a/lib/src/test/kotlin/org/tidepool/sdk/TestClass.kt +++ b/lib/src/test/kotlin/org/tidepool/sdk/TestClass.kt @@ -5,8 +5,6 @@ import kotlinx.coroutines.runBlocking import org.tidepool.sdk.model.auth.Realm import org.tidepool.sdk.model.data.BaseData import org.tidepool.sdk.model.data.ContinuousGlucoseData -import org.tidepool.sdk.requests.Data.CommaSeparatedArray -import org.tidepool.sdk.requests.TokenRequest import java.time.Instant import kotlin.test.Test @@ -15,15 +13,15 @@ class TestClass { fun THE_test() = runBlocking { val helper = CommunicationHelper(Environments.Qa2) launch { - val connectionResponse = helper.auth.obtainToken( - Realm.qa2, + val connectionResponse = helper.authApi.obtainToken( + Realm.Qa2, TokenRequest.createWithPassword("client_id", "username", "password") ) - val user = helper.users.getCurrentUserInfo(connectionResponse.access_token) - val allData = helper.data.getDataForUser( + val user = helper.usersApi.getCurrentUserInfo(connectionResponse.access_token) + val allData = helper.dataApi.getDataForUser( connectionResponse.access_token, user.userid, - types = CommaSeparatedArray(BaseData.DataType.cbg) + types = CommaSeparatedArray(BaseData.DataType.Cbg) ) for (data in allData) { (data as? ContinuousGlucoseData)?.run { diff --git a/lib/src/test/kotlin/org/tidepool/sdk/TidepoolSDKTest.kt b/lib/src/test/kotlin/org/tidepool/sdk/TidepoolSDKTest.kt new file mode 100644 index 0000000..e047e30 --- /dev/null +++ b/lib/src/test/kotlin/org/tidepool/sdk/TidepoolSDKTest.kt @@ -0,0 +1,51 @@ +package org.tidepool.sdk + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.koin.test.KoinTest + +class TidepoolSDKTest : KoinTest { + + @AfterEach + fun cleanup() { + TidepoolSDK.shutdown() + } + + @Test + fun `should initialize SDK successfully`() { + // Given + val environment = Environments.Dev1 + + // When + val sdk = TidepoolSDK.initialize(environment) + + // Then + assertNotNull(sdk) + assertEquals(sdk, TidepoolSDK.getInstance()) + } + + @Test + fun `should throw exception when accessing uninitialized SDK`() { + // Given - no initialization + + // When & Then + assertThrows(IllegalStateException::class.java) { + TidepoolSDK.getInstance() + } + } + + @Test + fun `should allow reinitialization after shutdown`() { + // Given + val sdk1 = TidepoolSDK.initialize(Environments.Dev1) + + // When + TidepoolSDK.shutdown() + val sdk2 = TidepoolSDK.initialize(Environments.Production) + + // Then + assertNotNull(sdk2) + assertEquals(sdk2, TidepoolSDK.getInstance()) + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/org/tidepool/sdk/auth/AuthSerializationTest.kt b/lib/src/test/kotlin/org/tidepool/sdk/auth/AuthSerializationTest.kt index 83d42bc..cff021c 100644 --- a/lib/src/test/kotlin/org/tidepool/sdk/auth/AuthSerializationTest.kt +++ b/lib/src/test/kotlin/org/tidepool/sdk/auth/AuthSerializationTest.kt @@ -1,11 +1,6 @@ package org.tidepool.sdk.auth import kotlinx.serialization.json.Json -import kotlinx.serialization.encodeToString -import kotlinx.serialization.decodeFromString -import org.tidepool.sdk.CommunicationHelper -import org.tidepool.sdk.requests.GrantType -import org.tidepool.sdk.requests.TokenRequest import kotlin.test.Test import kotlin.test.assertEquals @@ -16,7 +11,7 @@ class AuthSerializationTest { @Test fun serializationTest() { val req = TokenRequest( - GrantType.password, + GrantType.Password, "cgm-monitor", client_secret = "c50e6502-131c-47f0-b439-a43acb3b83d0", password = "qwertyuiop1234", @@ -32,7 +27,7 @@ class AuthSerializationTest { @Test fun serializationAnnotationTest() { - val req = TokenRequest(GrantType.tokenExchange, "cgm-monitor") + val req = TokenRequest(GrantType.TokenExchange, "cgm-monitor") val serialized = json.encodeToString(req) val expectedJson = "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:token-exchange\",\"client_id\":\"cgm-monitor\",\"client_secret\":null,\"subject_token\":null,\"subject_token_type\":null,\"requested_token_type\":null,\"subject_issuer\":null,\"username\":null,\"password\":null,\"code\":null,\"code_verifier\":null}" diff --git a/lib/src/test/kotlin/org/tidepool/sdk/deserialization/DeserializationTest.kt b/lib/src/test/kotlin/org/tidepool/sdk/deserialization/DeserializationTest.kt index 19539a5..c1f92e3 100644 --- a/lib/src/test/kotlin/org/tidepool/sdk/deserialization/DeserializationTest.kt +++ b/lib/src/test/kotlin/org/tidepool/sdk/deserialization/DeserializationTest.kt @@ -1,11 +1,9 @@ package org.tidepool.sdk.deserialization +import kotlinx.serialization.SerialName import kotlinx.serialization.json.Json -import kotlinx.serialization.encodeToString -import kotlinx.serialization.decodeFromString import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import org.tidepool.sdk.CommunicationHelper import kotlin.reflect.KClass import kotlin.test.Test import kotlin.test.assertEquals @@ -22,14 +20,17 @@ class DeserializationTest { @Serializable enum class SampleSubtype(override val subclassType: KClass) : ResultType { - - testSubclass(TestSubclass::class), - nullSubclass(Sample::class) + + @SerialName("testSubclass") + TestSubclassType(TestSubclass::class), + + @SerialName("nullSubclass") + NullSubclass(Sample::class) } } @Serializable - data class TestSubclass(val name: String = "Test") : Sample(SampleSubtype.testSubclass, "id") + data class TestSubclass(val name: String = "Test") : Sample(SampleSubtype.TestSubclassType, "id") val json: Json by lazy { CommunicationHelper.jsonConfig diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..da3e708 --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Sun Sep 28 17:08:16 CEST 2025 +sdk.dir=/Users/petrdavid/Library/Android/sdk diff --git a/settings.gradle.kts b/settings.gradle.kts index d55eaec..36b45b5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,3 +12,4 @@ plugins { rootProject.name = "TidepoolKotlinAPI" include("lib") +include("data")