Skip to content

Commit c60dcc2

Browse files
committed
Extract shared base class for SettingsDecoder and SettingsRemover, and add test case for not removing extra nullability marker keys
1 parent b815448 commit c60dcc2

File tree

2 files changed

+52
-85
lines changed

2 files changed

+52
-85
lines changed

multiplatform-settings-serialization/src/commonMain/kotlin/SerializationInternals.kt

Lines changed: 32 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,16 @@ internal class SettingsEncoder(
9090
}
9191

9292
@ExperimentalSerializationApi
93-
internal class SettingsDecoder(
94-
private val settings: Settings,
93+
internal abstract class AbstractSettingsDecoder(
94+
protected val settings: Settings,
9595
private val key: String,
9696
public override val serializersModule: SerializersModule
9797
) : AbstractDecoder() {
9898

9999
// Stacks of keys and indices so we can track index at arbitrary levels to know what we're decoding next
100100
private val keyStack = ArrayDeque<String>().apply { add(key) }
101101
private val indexStack = ArrayDeque<Int>().apply { add(0) }
102-
private fun getKey(): String = keyStack.joinToString(".")
102+
protected fun getKey(): String = keyStack.joinToString(".")
103103

104104
// Depth increases with beginStructure() and decreases with endStructure(). Subtly different from stack sizes.
105105
// This is important so we can tell whether the last items on the stack refer to the current parent or a sibling.
@@ -161,6 +161,27 @@ internal class SettingsDecoder(
161161
}
162162
}
163163

164+
// Hook to reset state after we throw during deserializationError()
165+
internal fun reset() {
166+
keyStack.clear()
167+
indexStack.clear()
168+
depth = 0
169+
keyStack.add(key)
170+
indexStack.add(0)
171+
}
172+
}
173+
174+
@ExperimentalSerializationApi
175+
internal class SettingsDecoder(
176+
settings: Settings,
177+
key: String,
178+
serializersModule: SerializersModule
179+
) : AbstractSettingsDecoder(
180+
settings,
181+
key,
182+
serializersModule
183+
) {
184+
164185
public override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
165186
settings.getIntOrNull("${getKey()}.size") ?: deserializationError()
166187

@@ -183,100 +204,26 @@ internal class SettingsDecoder(
183204
public override fun decodeLong(): Long = settings.getLongOrNull(getKey()) ?: deserializationError()
184205
public override fun decodeShort(): Short = settings.getIntOrNull(getKey())?.toShort() ?: deserializationError()
185206
public override fun decodeString(): String = settings.getStringOrNull(getKey()) ?: deserializationError()
186-
187-
// Hook to reset state after we throw during deserializationError()
188-
internal fun reset() {
189-
keyStack.clear()
190-
indexStack.clear()
191-
depth = 0
192-
keyStack.add(key)
193-
indexStack.add(0)
194-
}
195207
}
196208

197209
// (Ab)uses Decoder machinery to enumerate all keys related to a serialized value, so they can be removed
198210
@ExperimentalSerializationApi
199211
internal class SettingsRemover(
200-
private val settings: Settings,
201-
private val key: String,
202-
public override val serializersModule: SerializersModule
203-
) : AbstractDecoder() {
204-
212+
settings: Settings,
213+
key: String,
214+
serializersModule: SerializersModule
215+
) : AbstractSettingsDecoder(
216+
settings,
217+
key,
218+
serializersModule
219+
) {
205220
private val keys = mutableListOf<String>()
206221
fun removeKeys() {
207222
for (key in keys) {
208223
settings.remove(key)
209224
}
210225
}
211226

212-
// Stacks of keys and indices so we can track index at arbitrary levels to know what we're decoding next
213-
private val keyStack = ArrayDeque<String>().apply { add(key) }
214-
private val indexStack = ArrayDeque<Int>().apply { add(0) }
215-
private fun getKey(): String = keyStack.joinToString(".")
216-
217-
// Depth increases with beginStructure() and decreases with endStructure(). Subtly different from stack sizes.
218-
// This is important so we can tell whether the last items on the stack refer to the current parent or a sibling.
219-
private var depth = 0
220-
221-
222-
public override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
223-
if (keyStack.size > depth) {
224-
keyStack.removeLast()
225-
indexStack.removeLast()
226-
}
227-
228-
// Can usually ask descriptor for a size, except for collections
229-
val size = when (descriptor.kind) {
230-
StructureKind.LIST -> decodeCollectionSize(descriptor)
231-
StructureKind.MAP -> 2 * decodeCollectionSize(descriptor) // Maps look like lists [k1, v1, k2, v2, ...]
232-
else -> descriptor.elementsCount
233-
}
234-
235-
return getNextIndex(descriptor, size)
236-
}
237-
238-
private tailrec fun getNextIndex(descriptor: SerialDescriptor, size: Int): Int {
239-
val index = indexStack.removeLast()
240-
indexStack.addLast(index + 1)
241-
242-
return when {
243-
index >= size -> CompositeDecoder.DECODE_DONE
244-
isMissingAndOptional(descriptor, index) -> getNextIndex(descriptor, size)
245-
else -> {
246-
keyStack.add(descriptor.getElementName(index))
247-
indexStack.add(0)
248-
index
249-
}
250-
}
251-
}
252-
253-
private fun isMissingAndOptional(descriptor: SerialDescriptor, index: Int): Boolean {
254-
val key = "${getKey()}.${descriptor.getElementName(index)}"
255-
// Descriptor shows key is optional, key is not present, and nullability doesn't indicate key should be present
256-
val output = descriptor.isElementOptional(index) && descriptor.isNullable &&
257-
key !in settings && settings.getBooleanOrNull("$key?") != true
258-
keys.add(key)
259-
keys.add("$key?")
260-
return output
261-
}
262-
263-
264-
public override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
265-
depth++
266-
return super.beginStructure(descriptor)
267-
}
268-
269-
public override fun endStructure(descriptor: SerialDescriptor) {
270-
depth--
271-
keyStack.removeLast()
272-
indexStack.removeLast()
273-
if (keyStack.isEmpty()) {
274-
// We've reached the end of everything, so reset for potential decoder reuse
275-
keyStack.add(key)
276-
indexStack.add(0)
277-
}
278-
}
279-
280227
public override fun decodeCollectionSize(descriptor: SerialDescriptor): Int {
281228
val output = settings.getInt("${getKey()}.size", 0)
282229
keys.add("${getKey()}.size")

multiplatform-settings-serialization/src/commonTest/kotlin/SettingsSerializationTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,26 @@ class SettingsSerializationTest {
312312
assertEquals(0, settings.size)
313313
}
314314

315+
@Test
316+
fun removeValue_extra() {
317+
val delegate = mutableMapOf<String, Any>(
318+
"foo.a" to "hello",
319+
"foo.a?" to 0, // Should not be removed
320+
)
321+
val settings = MapSettings(delegate)
322+
323+
@Serializable
324+
data class Foo(
325+
val a: String,
326+
)
327+
328+
val foo = settings.decodeValueOrNull<Foo>("foo")
329+
assertEquals("hello", foo?.a)
330+
331+
settings.removeValue<Foo>("foo")
332+
assertEquals(mapOf<String, Any>("foo.a?" to 0), delegate)
333+
}
334+
315335
@Test
316336
fun containsValue() {
317337
val settings: Settings = MapSettings(

0 commit comments

Comments
 (0)