Skip to content

Commit

Permalink
Merge pull request #14 from ReneeVandervelde/data
Browse files Browse the repository at this point in the history
Add Data module for storage abstractions
  • Loading branch information
ReneeVandervelde authored Apr 16, 2024
2 parents 2c53af6 + 057bcd6 commit 6f62591
Show file tree
Hide file tree
Showing 49 changed files with 2,551 additions and 6 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Change Log
==========

1.3.0
-----

### Added

- New data module for for storage abstractions.
- Added a settings system to data module for key/value storage of
application configurations.

1.2.2
-----

Expand Down
3 changes: 2 additions & 1 deletion buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ repositories {
}

kotlin {
jvmToolchain(11)
jvmToolchain(17)
}
dependencies {
implementation(libs.kotlin.gradle)
implementation(libs.android.gradle)
implementation(libs.dokka)
implementation(libs.kotlinx.binary.compatibility)
implementation(libs.sqldelight.gradle)
}
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/multiplatform.android.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("android-library")
id("com.android.library")
kotlin("multiplatform")
}

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/multiplatform.tier1.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ repositories {
}

kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(11)
jvm()
js(IR) {
nodejs()
browser()
browser {
testTask {
useKarma {
Expand Down
499 changes: 499 additions & 0 deletions data/api/android/data.api

Large diffs are not rendered by default.

499 changes: 499 additions & 0 deletions data/api/jvm/data.api

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
plugins {
id("multiplatform.tier1")
id("multiplatform.android")
id("app.cash.sqldelight")
id("ink.publishing")
}

android {
namespace = "com.inkapplications.regolith.data"
}


sqldelight {
databases {
create("Settings") {
packageName.set("regolith.data")
schemaOutputDirectory.set(file("src/commonMain/sqldelight/databases"))
generateAsync = true
}
}
}

kotlin {
androidTarget {
publishAllLibraryVariants()
}

sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.coroutines.core)
implementation(libs.sqldelight.coroutines)
api(libs.watermelon.data)
}
}

val jvmMain by getting {
dependencies {
implementation(libs.sqldelight.jvm)
}
}

val androidMain by getting {
dependencies {
implementation(libs.sqldelight.android)
}
}

val nativeMain by getting {
dependencies {
implementation(libs.sqldelight.native)
}
}

val jsMain by getting {
dependencies {
implementation(libs.sqldelight.web.worker.driver)
implementation(devNpm("copy-webpack-plugin", "9.1.0"))
implementation(npm("sql.js", "1.6.2"))
implementation(devNpm("copy-webpack-plugin", "9.1.0"))
}
}

val commonTest by getting {
dependencies {
implementation(libs.kotlin.test.core)
implementation(libs.kotlin.test.junit)
implementation(libs.coroutines.test)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package regolith.data.settings

import android.content.Context
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import regolith.data.Settings

/**
* Main entry-point for creating settings services on Android.
*/
class AndroidSettingsModule(
context: Context,
migrationScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) {
private val driver = AndroidSqliteDriver(Settings.Schema.synchronous(), context, "settings.db")

/**
* Read/write access to the settings database.
*/
val settingsAccess = createDatabaseAccess(migrationScope, driver)
}
26 changes: 26 additions & 0 deletions data/src/commonMain/kotlin/regolith/data/settings/Described.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package regolith.data.settings

/**
* Fields to allow a setting to be displayed in a UI
*/
interface Described {
/**
* A user-readable name for the setting.
*/
val name: String

/**
* Visibility level of the setting. Used to show/hide advanced settings.
*/
val level: SettingLevel

/**
* An optional user-readable category name that can be used for grouping.
*/
val category: SettingCategory?

/**
* An optional user-readable description of the setting.
*/
val description: String?
}
13 changes: 13 additions & 0 deletions data/src/commonMain/kotlin/regolith/data/settings/Keyed.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package regolith.data.settings

/**
* Object that identifies the key in a key/value pair.
*/
interface Keyed {
/**
* A unique key that identifies the setting.
*
* This key should never change in order to preserve data.
*/
val key: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package regolith.data.settings

/**
* A named group of settings.
*
* The [key] and [name] can be the same, provided that the name is
* unique per category. This can be useful if the name will be generated
* from localization tools.
*/
data class SettingCategory(
val key: String,
val name: String,
) {
constructor(key: String): this(key, key)
}
36 changes: 36 additions & 0 deletions data/src/commonMain/kotlin/regolith/data/settings/SettingLevel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package regolith.data.settings

/**
* Visibility of the setting.
*
* This value can be used to control which settings are available in
* a settings panel.
*/
enum class SettingLevel {
/**
* Setting is visible to any user in the system.
*/
User,

/**
* Setting is visible to users, but in an 'advanced' section or flagged
* appropriately.
*/
Advanced,

/**
* Setting should only be hidden from normal users, but visible for
* development and debugging.
*/
Dev,

/**
* Setting should be hidden entirely from the user interface and only
* controlled programmatically.
*/
Hidden;

companion object {
val DEFAULT = User
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package regolith.data.settings

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import regolith.data.settings.structure.DataSetting
import regolith.data.settings.structure.PrimitiveSetting

/**
* Provides read/write access to settings.
*/
interface SettingsAccess {
/**
* Observe changes to a setting.
*/
fun <STORED> observeSetting(setting: PrimitiveSetting<STORED>): Flow<STORED>

/**
* Get the current value of a setting.
*/
suspend fun <STORED> getSetting(setting: PrimitiveSetting<STORED>): STORED

/**
* Write a new value to a setting.
*/
suspend fun <STORED> writeSetting(setting: PrimitiveSetting<STORED>, value: STORED)
}

/**
* Get the current value of a data setting.
*/
suspend fun <STORED, DATA> SettingsAccess.getSetting(setting: DataSetting<STORED, DATA>): DATA {
return getSetting(setting.toPrimitive()).let { setting.dataTransformer.transform(it) }
}

/**
* Write a new value to a data setting.
*/
suspend fun <STORED, DATA> SettingsAccess.writeSetting(setting: DataSetting<STORED, DATA>, value: DATA) {
writeSetting(setting.toPrimitive(), setting.dataTransformer.reverseTransform(value))
}

/**
* Observe changes to a data setting.
*/
fun <STORED, DATA> SettingsAccess.observeSetting(setting: DataSetting<STORED, DATA>): Flow<DATA> {
return observeSetting(setting.toPrimitive()).map { setting.dataTransformer.transform(it) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package regolith.data.settings

import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.db.SqlDriver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import regolith.data.Settings
import regolith.data.settings.structure.*

internal class SettingsDatabaseAccess(
private val database: Deferred<Settings>,
): SettingsAccess {
override fun <STORED> observeSetting(setting: PrimitiveSetting<STORED>): Flow<STORED> {
val queries = flow { emit(database.await().settingsQueries) }
return when (setting) {
is StringSetting -> queries.flatMapLatest { it.getStringValue(setting.key).asFlow() }
.map { it.executeAsOneOrNull() }
.map { it?.stringValue }
is LongSetting -> queries.flatMapLatest { it.getIntValue(setting.key).asFlow() }
.map { it.executeAsOneOrNull() }
.map { it?.intValue }
is DoubleSetting -> queries.flatMapLatest { it.getRealValue(setting.key).asFlow() }
.map { it.executeAsOneOrNull() }
.map { it?.realValue }
is BlobSetting -> queries.flatMapLatest { it.getBlobValue(setting.key).asFlow() }
.map { it.executeAsOneOrNull() }
.map { it?.blobValue }
}.map { it as STORED }
}

override suspend fun <STORED> getSetting(setting: PrimitiveSetting<STORED>): STORED {
return when (setting) {
is StringSetting -> database.await().settingsQueries.getStringValue(setting.key)
.executeAsOneOrNull()
?.stringValue
is LongSetting -> database.await().settingsQueries.getIntValue(setting.key)
.executeAsOneOrNull()
?.intValue
is DoubleSetting -> database.await().settingsQueries.getRealValue(setting.key)
.executeAsOneOrNull()
?.realValue
is BlobSetting -> database.await().settingsQueries.getBlobValue(setting.key)
.executeAsOneOrNull()
?.blobValue
} as STORED
}

override suspend fun <STORED> writeSetting(setting: PrimitiveSetting<STORED>, value: STORED) {
when (setting) {
is StringSetting -> database.await().settingsQueries.setStringValue(
key = setting.key,
stringValue = value as String?,
)
is LongSetting -> database.await().settingsQueries.setIntValue(
key = setting.key,
intValue = value as Long?,
)
is DoubleSetting -> database.await().settingsQueries.setRealValue(
key = setting.key,
realValue = value as Double?,
)
is BlobSetting -> database.await().settingsQueries.setBlobValue(
key = setting.key,
blobValue = value as ByteArray?,
)
}
}
}

internal fun createDatabaseAccess(scope: CoroutineScope, driver: SqlDriver): SettingsAccess {
val database = scope.async {
Settings.Schema.create(driver).await()
Settings(driver)
}

return SettingsDatabaseAccess(database)
}
13 changes: 13 additions & 0 deletions data/src/commonMain/kotlin/regolith/data/settings/Validated.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package regolith.data.settings

import com.inkapplications.data.validator.DataValidator

/**
* Constructs used for data validation
*/
interface Validated<DATA> {
/**
* Validator to be used on user-supplied input fields before storage.
*/
val inputValidator: DataValidator<DATA>
}
Loading

0 comments on commit 6f62591

Please sign in to comment.