diff --git a/README.md b/README.md index 640f37e..3d65275 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ [ ![Download](https://api.bintray.com/packages/elementsinteractive/maven/ObjectStore/images/download.svg) ](https://bintray.com/elementsinteractive/maven/ObjectStore/_latestVersion) # ObjectStore + +[![kotlin](https://img.shields.io/badge/kotlin-1.3.31-blue.svg)]() + ###### Convenient interface for persisting objects. ``` groovy implementation "nl.elements.objectstore:objectstore:+" @@ -19,6 +22,21 @@ fun example(store: ObjectStore) { } ``` +#### Define a model + +```kotlin +class AppData : ObjectStoreModel(InMemoryStore()) { + var username by stringItem() + val lastname by nullableStringItem() + var age by intItem() + var hasSeenOnboarding by booleanItem() + val landedOnDashboard by booleanNullabeItem() + var lastSeen by longItem() + var rating by floatItem() +} +``` + + ## Observing Each `ObjectStore` is (Rx) observable and will emit whenever something changes in store. diff --git a/library/src/androidTest/java/nl/elements/objectstore/PreferencesStoreTest.kt b/library/src/androidTest/java/nl/elements/objectstore/PreferencesStoreTest.kt index e5a0aa8..7866c18 100644 --- a/library/src/androidTest/java/nl/elements/objectstore/PreferencesStoreTest.kt +++ b/library/src/androidTest/java/nl/elements/objectstore/PreferencesStoreTest.kt @@ -28,7 +28,7 @@ class PreferencesStoreTest { val key = "contains" val name = "Danny" - store.set(key, name) + store[key] = name assertTrue(store.contains(key)) } @@ -92,7 +92,7 @@ class PreferencesStoreTest { val key = "name" val value = "Danny" - store.set(key, value) + store[key] = value val actual = store.keys @@ -102,7 +102,7 @@ class PreferencesStoreTest { } } -private fun ObjectStore.setAndGet(key: String, value: T): T { +private fun ObjectStore.setAndGet(key: String, value: T): T? { set(key, value) return this[key] } diff --git a/library/src/main/java/nl/elements/objectstore/Converter.kt b/library/src/main/java/nl/elements/objectstore/Converter.kt index 95737e1..9f243ec 100644 --- a/library/src/main/java/nl/elements/objectstore/Converter.kt +++ b/library/src/main/java/nl/elements/objectstore/Converter.kt @@ -15,24 +15,24 @@ interface Converter { * Convert the value into bytes that will be used for IO. */ - fun serialize(key: String, value: Any, output: OutputStream) + fun serialize(key: String, value: Any?, output: OutputStream) /** * Convert the bytes from IO into the desired object. */ - fun deserialize(key: String, input: InputStream): T + fun deserialize(key: String, input: InputStream): T? companion object { val DEFAULT = object : Converter { - override fun serialize(key: String, value: Any, output: OutputStream) = + override fun serialize(key: String, value: Any?, output: OutputStream) = ObjectOutputStream(output).writeObject(value) @Suppress("UNCHECKED_CAST") - override fun deserialize(key: String, input: InputStream): T = - ObjectInputStream(input).readObject() as T + override fun deserialize(key: String, input: InputStream): T? = + ObjectInputStream(input).readObject() as T? } diff --git a/library/src/main/java/nl/elements/objectstore/ObjectStore.kt b/library/src/main/java/nl/elements/objectstore/ObjectStore.kt index 8003ec5..a762109 100644 --- a/library/src/main/java/nl/elements/objectstore/ObjectStore.kt +++ b/library/src/main/java/nl/elements/objectstore/ObjectStore.kt @@ -18,18 +18,18 @@ interface ObjectStore : ReadableObjectStore, ObservableSource val converter: Converter val transformer: Transformer - operator fun set(key: String, value: Any) + operator fun set(key: String, value: Any?) fun remove(key: String) - fun OutputStream.write(key: String, value: Any) = + fun OutputStream.write(key: String, value: Any?) = ByteArrayOutputStream() .also { converter.serialize(key, value, it) } .toByteArray() .inputStream() .let { transformer.write(key, it, this@write) } - fun InputStream.read(key: String): T = + fun InputStream.read(key: String): T? = ByteArrayOutputStream() .also { transformer.read(key, this@read, it) } .toByteArray() @@ -57,7 +57,7 @@ fun ObjectStore.toObservable(): Observable = Observable.defer fun ObjectStore.toReadableObjectStore(): ReadableObjectStore = this -fun ObjectStore.writeToBytes(key: String, value: Any): ByteArray = +fun ObjectStore.writeToBytes(key: String, value: Any?): ByteArray = ByteArrayOutputStream() .also { it.write(key, value) } .toByteArray() @@ -114,13 +114,13 @@ private fun ObjectStore.withNamespace(namespace: String, next: ObjectStore): Obj override val keys: Set get() = store.keys.map { namespace + it }.toMutableSet().apply { addAll(next.keys) } - override fun get(key: String): T = + override fun get(key: String): T? = key.removeNamespace()?.let(store::get) ?: next[key] override fun contains(key: String): Boolean = key.removeNamespace()?.let(store::contains) ?: next.contains(key) - override fun set(key: String, value: Any) = + override fun set(key: String, value: Any?) = key.removeNamespace()?.let { store[it] = value } ?: next.set(key, value) override fun remove(key: String) = key.removeNamespace()?.let(store::remove) ?: next.remove(key) @@ -148,7 +148,7 @@ private fun unknownNamespaceObjectStore(): ObjectStore = object : ObjectStore { override val transformer: Transformer = Transformer.DEFAULT override val keys: Set = emptySet() - override fun set(key: String, value: Any) = throw UnknownNamespaceException(key) + override fun set(key: String, value: Any?) = throw UnknownNamespaceException(key) override fun remove(key: String) = throw UnknownNamespaceException(key) diff --git a/library/src/main/java/nl/elements/objectstore/ObjectStoreModel.kt b/library/src/main/java/nl/elements/objectstore/ObjectStoreModel.kt new file mode 100644 index 0000000..1e58439 --- /dev/null +++ b/library/src/main/java/nl/elements/objectstore/ObjectStoreModel.kt @@ -0,0 +1,86 @@ +package nl.elements.objectstore + +import nl.elements.objectstore.model.AnyItem +import nl.elements.objectstore.model.AnyNullableItem +import kotlin.properties.ReadWriteProperty + +abstract class ObjectStoreModel( + internal val store: ObjectStore +) { + /** + * Clears all items in this store + */ + fun clear() { + store.keys.forEach(store::remove) + } + + /** + * Delegate a string item + * @param default value + * @param key custom key (optional) + */ + protected fun stringItem( + default: String = "", + key: String? = null + ): ReadWriteProperty = AnyItem(default, key) + + /** + * Delegate a nullable string item + * @param default value + * @param key custom key (optional) + */ + protected fun nullableStringItem( + default: String? = null, + key: String? = null + ): ReadWriteProperty = AnyNullableItem(default, key) + + /** + * Delegate a boolean item + * @param default value + * @param key custom key (optional) + */ + protected fun booleanItem( + default: Boolean = false, + key: String? = null + ): ReadWriteProperty = AnyItem(default, key) + + /** + * Delegate a nullable boolean item + * @param default value + * @param key custom key (optional) + */ + protected fun booleanNullabeItem( + default: Boolean? = null, + key: String? = null + ): ReadWriteProperty = AnyNullableItem(default, key) + + /** + * Delegate an int item + * @param default value + * @param key custom key (optional) + */ + protected fun intItem( + default: Int = 0, + key: String? = null + ): ReadWriteProperty = AnyItem(default, key) + + /** + * Delegate a long item + * @param default value + * @param key custom key (optional) + */ + protected fun longItem( + default: Long = 0L, + key: String? = null + ): ReadWriteProperty = AnyItem(default, key) + + /** + * Delegate a float item + * @param default value + * @param key custom key (optional) + */ + protected fun floatItem( + default: Float = 0F, + key: String? = null + ): ReadWriteProperty = AnyItem(default, key) +} diff --git a/library/src/main/java/nl/elements/objectstore/ReadableObjectStore.kt b/library/src/main/java/nl/elements/objectstore/ReadableObjectStore.kt index f5870bb..4c1a152 100644 --- a/library/src/main/java/nl/elements/objectstore/ReadableObjectStore.kt +++ b/library/src/main/java/nl/elements/objectstore/ReadableObjectStore.kt @@ -4,7 +4,7 @@ interface ReadableObjectStore { val keys: Set - operator fun get(key: String): T + operator fun get(key: String): T? operator fun contains(key: String): Boolean @@ -39,7 +39,7 @@ private fun combine(l: ReadableObjectStore, r: ReadableObjectStore): ReadableObj override val keys: Set get() = l.keys.toMutableSet().apply { addAll(r.keys) } - override fun get(key: String): T = + override fun get(key: String): T? = when (key) { in l -> l[key] else -> r[key] diff --git a/library/src/main/java/nl/elements/objectstore/model/AbstractItem.kt b/library/src/main/java/nl/elements/objectstore/model/AbstractItem.kt new file mode 100644 index 0000000..9c0cfdd --- /dev/null +++ b/library/src/main/java/nl/elements/objectstore/model/AbstractItem.kt @@ -0,0 +1,10 @@ +package nl.elements.objectstore.model + +import nl.elements.objectstore.ObjectStoreModel +import kotlin.properties.ReadWriteProperty + +abstract class AbstractItem : ReadWriteProperty, ItemKey { +} + +abstract class NullableAbstractItem : ReadWriteProperty, ItemKey { +} diff --git a/library/src/main/java/nl/elements/objectstore/model/AnyItem.kt b/library/src/main/java/nl/elements/objectstore/model/AnyItem.kt new file mode 100644 index 0000000..5730e88 --- /dev/null +++ b/library/src/main/java/nl/elements/objectstore/model/AnyItem.kt @@ -0,0 +1,23 @@ +package nl.elements.objectstore.model + +import nl.elements.objectstore.ObjectStoreModel +import kotlin.reflect.KProperty + +internal class AnyItem( + val default: T, + override val key: String? +) : AbstractItem() { + + override fun getValue(thisRef: ObjectStoreModel, property: KProperty<*>): T = + (key ?: property.name).let { key -> + if (thisRef.store.contains(key)) { + thisRef.store[key] ?: default + } else { + default + } + } + + override fun setValue(thisRef: ObjectStoreModel, property: KProperty<*>, value: T) { + thisRef.store[key ?: property.name] = value + } +} diff --git a/library/src/main/java/nl/elements/objectstore/model/AnyNullableItem.kt b/library/src/main/java/nl/elements/objectstore/model/AnyNullableItem.kt new file mode 100644 index 0000000..798a2d4 --- /dev/null +++ b/library/src/main/java/nl/elements/objectstore/model/AnyNullableItem.kt @@ -0,0 +1,23 @@ +package nl.elements.objectstore.model + +import nl.elements.objectstore.ObjectStoreModel +import kotlin.reflect.KProperty + +internal class AnyNullableItem( + val default: T?, + override val key: String? +) : NullableAbstractItem() { + + override fun getValue(thisRef: ObjectStoreModel, property: KProperty<*>): T? = + (key ?: property.name).let { key -> + if (thisRef.store.contains(key)) { + thisRef.store[key] ?: default + } else { + default + } + } + + override fun setValue(thisRef: ObjectStoreModel, property: KProperty<*>, value: T?) { + thisRef.store[key ?: property.name] = value + } +} diff --git a/library/src/main/java/nl/elements/objectstore/model/ItemKey.kt b/library/src/main/java/nl/elements/objectstore/model/ItemKey.kt new file mode 100644 index 0000000..6758257 --- /dev/null +++ b/library/src/main/java/nl/elements/objectstore/model/ItemKey.kt @@ -0,0 +1,5 @@ +package nl.elements.objectstore.model + +interface ItemKey { + val key: String? +} diff --git a/library/src/main/java/nl/elements/objectstore/stores/DatabaseStore.kt b/library/src/main/java/nl/elements/objectstore/stores/DatabaseStore.kt index 2840922..27948ca 100644 --- a/library/src/main/java/nl/elements/objectstore/stores/DatabaseStore.kt +++ b/library/src/main/java/nl/elements/objectstore/stores/DatabaseStore.kt @@ -36,14 +36,14 @@ class DatabaseStore( """ CREATE TABLE IF NOT EXISTS $table ( $_ID STRING PRIMARY KEY UNIQUE, - $VALUE BLOB NOT NULL + $VALUE BLOB ) """ database.rawQuery(query, null).close() } - override fun set(key: String, value: Any) { + override fun set(key: String, value: Any?) { val bytes = writeToBytes(key, value) val values = ContentValues().apply { put(_ID, key) @@ -54,13 +54,13 @@ class DatabaseStore( emit(Updated(key)) } - override fun get(key: String): T = + override fun get(key: String): T? = queryById(key, VALUE).use { cursor -> cursor .takeIf { it.moveToFirst() } ?.value ?.inputStream() - ?.read(key)!! + ?.read(key) } override fun contains(key: String): Boolean = diff --git a/library/src/main/java/nl/elements/objectstore/stores/DirectoryStore.kt b/library/src/main/java/nl/elements/objectstore/stores/DirectoryStore.kt index d0fc1cc..5049cda 100644 --- a/library/src/main/java/nl/elements/objectstore/stores/DirectoryStore.kt +++ b/library/src/main/java/nl/elements/objectstore/stores/DirectoryStore.kt @@ -24,7 +24,7 @@ class DirectoryStore( override val keys: Set get() = directory.list().toSet() - override fun set(key: String, value: Any) { + override fun set(key: String, value: Any?) { fileOf(key) .ensure() .outputStream() @@ -33,7 +33,7 @@ class DirectoryStore( emit(Updated(key)) } - override fun get(key: String): T = + override fun get(key: String): T? = fileOf(key) .ensure() .inputStream() diff --git a/library/src/main/java/nl/elements/objectstore/stores/MemoryStore.kt b/library/src/main/java/nl/elements/objectstore/stores/MemoryStore.kt index b0f57e8..31196bd 100644 --- a/library/src/main/java/nl/elements/objectstore/stores/MemoryStore.kt +++ b/library/src/main/java/nl/elements/objectstore/stores/MemoryStore.kt @@ -17,12 +17,12 @@ class MemoryStore( override val keys: Set get() = synchronized { data.keys.toSet() } - override fun get(key: String): T = + override fun get(key: String): T? = synchronized { data[key]!! } .inputStream() .read(key) - override fun set(key: String, value: Any) { + override fun set(key: String, value: Any?) { val bytes = writeToBytes(key, value) synchronized { data[key] = bytes } diff --git a/library/src/main/java/nl/elements/objectstore/stores/PreferencesStore.kt b/library/src/main/java/nl/elements/objectstore/stores/PreferencesStore.kt index 5045807..8ef0e5e 100644 --- a/library/src/main/java/nl/elements/objectstore/stores/PreferencesStore.kt +++ b/library/src/main/java/nl/elements/objectstore/stores/PreferencesStore.kt @@ -24,7 +24,7 @@ class PreferencesStore( override val keys: Set get() = preferences.all.keys - override fun set(key: String, value: Any) { + override fun set(key: String, value: Any?) { writeToBytes(key, value) .let { Base64.encodeToString(it, 0) } .let { preferences.edit().putString(key, it).apply() } @@ -32,7 +32,7 @@ class PreferencesStore( emit(Updated(key)) } - override fun get(key: String): T = + override fun get(key: String): T? = preferences .getString(key, null)!! .let { Base64.decode(it, 0) } @@ -42,8 +42,9 @@ class PreferencesStore( override fun contains(key: String): Boolean = preferences.contains(key) override fun remove(key: String) { - if (preferences.edit().remove(key).commit()) + if (preferences.edit().remove(key).commit()) { emit(Removed(key)) + } } fun toPreferences(): SharedPreferences = StorePreferences(this, preferences) @@ -61,17 +62,17 @@ private class StorePreferences( override fun contains(key: String?): Boolean = key?.let { store.contains(it) } ?: false - override fun getBoolean(key: String?, defValue: Boolean): Boolean = get(key, defValue) + override fun getBoolean(key: String?, defValue: Boolean): Boolean = get(key, defValue) ?: defValue - override fun getInt(key: String?, defValue: Int): Int = get(key, defValue) + override fun getInt(key: String?, defValue: Int): Int = get(key, defValue) ?: defValue - override fun getLong(key: String?, defValue: Long): Long = get(key, defValue) + override fun getLong(key: String?, defValue: Long): Long = get(key, defValue) ?: defValue - override fun getFloat(key: String?, defValue: Float): Float = get(key, defValue) + override fun getFloat(key: String?, defValue: Float): Float = get(key, defValue) ?: defValue - override fun getString(key: String?, defValue: String): String? = get(key, defValue) + override fun getString(key: String, defValue: String?): String? = get(key, defValue) - override fun getStringSet(key: String?, defValues: MutableSet): MutableSet? = get(key, defValues) + override fun getStringSet(key: String, defValues: MutableSet?): MutableSet? = get(key, defValues) override fun getAll(): MutableMap = store @@ -100,7 +101,7 @@ private class StorePreferences( } } - private fun get(key: String?, defValue: T): T = key?.let { store.get(it) } ?: defValue + private fun get(key: String?, defValue: T?): T? = key?.let { store.get(it) } ?: defValue } diff --git a/library/src/test/java/nl/elements/objectstore/InMemoryStoreTest.kt b/library/src/test/java/nl/elements/objectstore/InMemoryStoreTest.kt index 1b05383..9f54ad4 100644 --- a/library/src/test/java/nl/elements/objectstore/InMemoryStoreTest.kt +++ b/library/src/test/java/nl/elements/objectstore/InMemoryStoreTest.kt @@ -21,7 +21,7 @@ class InMemoryStoreTest { val key = "key" val value = "Danny" - store.set(key, value) + store[key] = value assertTrue(store.contains(key)) } @@ -85,7 +85,7 @@ class InMemoryStoreTest { val key = "name" val value = "Danny" - store.set(key, value) + store[key] = value val actual = store.keys @@ -95,7 +95,7 @@ class InMemoryStoreTest { } } -private fun ObjectStore.setAndGet(key: String, value: T): T { +private fun ObjectStore.setAndGet(key: String, value: T): T? { set(key, value) return this[key] } diff --git a/library/src/test/java/nl/elements/objectstore/ObjectStoreModelTest.kt b/library/src/test/java/nl/elements/objectstore/ObjectStoreModelTest.kt new file mode 100644 index 0000000..dba2d4b --- /dev/null +++ b/library/src/test/java/nl/elements/objectstore/ObjectStoreModelTest.kt @@ -0,0 +1,80 @@ +package nl.elements.objectstore + +import nl.elements.objectstore.stores.MemoryStore +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ObjectStoreModelTest { + + @Test + fun `Test Store Model with MemoryStore`() { + val model = InMemoryModel() + + assertEquals("", model.name) + assertEquals(DEFAULT_STRING_VALUE, model.nameDefault) + + assertNull(model.nameNull) + assertEquals(DEFAULT_STRING_VALUE, model.nameNullDefault) + + assertFalse(model.isOpen) + assertFalse(model.isOpenDefault) + + assertNull(model.isClosedNull) + assertEquals(DEFAULT_BOOLEAN_VALUE, model.isClosedNullDefault) + + assertEquals(0, model.int) + assertEquals(DEFAULT_INT_VALUE, model.intDefault) + + assertEquals(0, model.long) + assertEquals(DEFAULT_LONG_VALUE, model.longDefault) + + assertEquals(0F, model.float) + assertEquals(DEFAULT_FLOAT_VALUE, model.floatDefault) + } + + @Test + fun `Test Store Model with MemoryStore to change values`() { + val model = InMemoryModel() + + model.isOpen = true + model.name = "Test" + + assertTrue(model.isOpen) + assertEquals("Test", model.name) + } + + + companion object { + class InMemoryModel : ObjectStoreModel(MemoryStore()) { + var name by stringItem() + var nameDefault by stringItem(DEFAULT_STRING_VALUE) + + var nameNull by nullableStringItem() + var nameNullDefault by nullableStringItem(DEFAULT_STRING_VALUE) + + var isOpen by booleanItem() + var isOpenDefault by booleanItem(false) + + var isClosedNull by booleanNullabeItem() + var isClosedNullDefault by booleanNullabeItem(DEFAULT_BOOLEAN_VALUE) + + var int by intItem() + var intDefault by intItem(DEFAULT_INT_VALUE) + + var long by longItem() + var longDefault by longItem(DEFAULT_LONG_VALUE) + + var float by floatItem() + var floatDefault by floatItem(DEFAULT_FLOAT_VALUE) + } + + const val DEFAULT_STRING_VALUE = "default" + const val DEFAULT_BOOLEAN_VALUE = true + const val DEFAULT_INT_VALUE = 42 + const val DEFAULT_LONG_VALUE = 42L + const val DEFAULT_FLOAT_VALUE = 42F + } +}