/
types.kt
128 lines (115 loc) · 6.51 KB
/
types.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package net.aquadc.persistence.android.pref
import android.content.SharedPreferences
import android.util.Base64
import net.aquadc.collections.contains
import net.aquadc.collections.plus
import net.aquadc.persistence.fatMapTo
import net.aquadc.persistence.struct.FieldDef
import net.aquadc.persistence.struct.Schema
import net.aquadc.persistence.type.DataType
import net.aquadc.persistence.type.serialized
import net.aquadc.properties.internal.Unset
import java.lang.Double as JavaLangDouble
internal fun <SCH : Schema<SCH>, T> SCH.get(what: FieldDef<SCH, T, *>, prefs: SharedPreferences): T {
val value = typeOf(what as FieldDef<SCH, T, DataType<T>>).get(prefs, nameOf(what).toString())
return if (value !== Unset) value else
defaultOrElse(what) { throw NoSuchElementException(what.toString()/* no value in shared prefs and no default */) }
}
internal fun <T> DataType<T>.get(prefs: SharedPreferences, name: String, default: T): T {
val value = get(prefs, name)
return if (value === Unset) default else value
}
/**
* SharedPrefs do not support storing null values. Null means 'absent' in this context.
* To preserve consistent behaviour of default field values amongst nullable and non-nullable fields,
* we store 'null' ourselves. If a field has String type, 'null' is stored as Boolean 'false'.
* Otherwise 'null' is stored as a String "null".
*/
@JvmSynthetic internal val storedAsString = DataType.NotNull.Simple.Kind.Str + DataType.NotNull.Simple.Kind.Blob
@Suppress("UNCHECKED_CAST")
private fun <T> DataType<T>.get(prefs: SharedPreferences, key: String): T {
if (!prefs.contains(key)) return Unset as T
val map = prefs.all // sadly, copying prefs fully is the only way to achieve correctness concurrently
val value = map[key]
if (value === null) return Unset as T
val type = if (this is DataType.Nullable<*, *>) {
val act = actualType
when (act) {
is DataType.Nullable<*, *> -> throw AssertionError()
is DataType.NotNull.Simple<*> -> if (value == (if (act.kind in storedAsString) false else "null")) return null as T
is DataType.NotNull.Collect<*, *, *>, is DataType.NotNull.Partial<*, *> -> if (value == false) return null as T
}
act as DataType<T/*!!*/>
} else this
return when (type) {
is DataType.Nullable<*, *> -> throw AssertionError()
is DataType.NotNull.Simple -> type.load(
if (type.hasStringRepresentation) value as CharSequence
else when (type.kind) {
DataType.NotNull.Simple.Kind.Bool -> value as Boolean
DataType.NotNull.Simple.Kind.I32 -> value as Int
DataType.NotNull.Simple.Kind.I64 -> value as Long
DataType.NotNull.Simple.Kind.F32 -> value as Float
DataType.NotNull.Simple.Kind.F64 -> JavaLangDouble.longBitsToDouble(value as Long)
DataType.NotNull.Simple.Kind.Str -> value as String
DataType.NotNull.Simple.Kind.Blob -> Base64.decode(value as String, Base64.DEFAULT)
else -> throw AssertionError()
}
)
is DataType.NotNull.Collect<T, *, *> -> type.elementType.let { elementType ->
if (elementType is DataType.NotNull.Simple<*> &&
(elementType.hasStringRepresentation || elementType.kind == DataType.NotNull.Simple.Kind.Str)) // TODO should store everything in strings
type.load((value as Set<String>).map(elementType::load)) // todo zero-copy
else /* here we have a Collection<Whatever>, including potentially a collection of collections, structs, etc */
serialized(type).load(Base64.decode(value as String, Base64.DEFAULT))
}
is DataType.NotNull.Partial<*, *> -> serialized(type).load(Base64.decode(value as String, Base64.DEFAULT))
}
}
internal fun <T> DataType<T>.put(editor: SharedPreferences.Editor, key: String, value: T) {
val type = if (this is DataType.Nullable<*, *>) {
val act = actualType
if (value == null) when (act) {
is DataType.Nullable<*, *> ->
throw AssertionError()
is DataType.NotNull.Simple<*> ->
if (act.kind in storedAsString) editor.putBoolean(key, false) else editor.putString(key, "null")
is DataType.NotNull.Collect<*, *, *> ->
editor.putBoolean(key, false)
is DataType.NotNull.Partial<*, *> ->
editor.putBoolean(key, false)
}.also { return }
act as DataType<T/*!!*/>
} else this
when (type) {
is DataType.Nullable<*, *> -> throw AssertionError()
is DataType.NotNull.Simple<T> ->
if (type.hasStringRepresentation) editor.putString(key, type.storeAsString(value).toString())
else type.store(value).let { v -> when (type.kind) {
DataType.NotNull.Simple.Kind.Bool -> editor.putBoolean(key, v as Boolean)
DataType.NotNull.Simple.Kind.I32 -> editor.putInt(key, v as Int)
DataType.NotNull.Simple.Kind.I64 -> editor.putLong(key, v as Long)
DataType.NotNull.Simple.Kind.F32 -> editor.putFloat(key, v as Float)
DataType.NotNull.Simple.Kind.F64 -> editor.putLong(key, java.lang.Double.doubleToLongBits(v as Double))
DataType.NotNull.Simple.Kind.Str -> editor.putString(key, v as String)
DataType.NotNull.Simple.Kind.Blob -> editor.putString(key, Base64.encodeToString(v as ByteArray, Base64.DEFAULT))
else -> throw AssertionError()
} }
is DataType.NotNull.Collect<T, *, *> -> type.elementType.let { elementType ->
if (elementType is DataType.NotNull.Simple && (elementType.hasStringRepresentation || elementType.kind == DataType.NotNull.Simple.Kind.Str))
editor.putStringSet(
key,
type.store(value)
.fatMapTo<HashSet<String>, T, String>(HashSet()) { v ->
(elementType as DataType.NotNull.Simple<T>)
.let { if (it.hasStringRepresentation) it.storeAsString(v) else it.store(v) as CharSequence }
.toString()
}
)
else
editor.putString(key, Base64.encodeToString(serialized(type).store(value) as ByteArray, Base64.DEFAULT))
}
is DataType.NotNull.Partial<*, *> ->
editor.putString(key, Base64.encodeToString(serialized(type).store(value) as ByteArray, Base64.DEFAULT))
}
}