Repository: config4k/config4k
Pull request #291 (2025-02-21 15:25:04)

`com.typesafe.config.ConfigMemorySize` should be contextually-serializable
As part of #280 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/config4k/config4k/pull/291?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=config4k) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.56%. Comparing base [(`f980619`)](https://app.codecov.io/gh/config4k/config4k/commit/f9806199d16f9c0c117042c32e0d320d916d1f64?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=config4k) to head [(`71ff69c`)](https://app.codecov.io/gh/config4k/config4k/commit/71ff69ca54512c4ced13fbb041caba2c0769a78b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=config4k).

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main     #291      +/-   ##
============================================
+ Coverage     88.52%   88.56%   +0.03%     
  Complexity      110      110              
============================================
  Files            38       38              
  Lines           340      341       +1     
  Branches         88       88              
============================================
+ Hits            301      302       +1     
  Misses           22       22              
  Partials         17       17              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/config4k/config4k/pull/291?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=config4k).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=config4k).


Patch:
diff --git a/src/main/kotlin/io/github/config4k/Config4k.kt b/src/main/kotlin/io/github/config4k/Config4k.kt
index 015c8c8..91ec769 100644
--- a/src/main/kotlin/io/github/config4k/Config4k.kt
+++ b/src/main/kotlin/io/github/config4k/Config4k.kt
@@ -5,6 +5,7 @@ import io.github.config4k.serializers.ConfigValueSerializer
 import io.github.config4k.serializers.PeriodSerializer
 import io.github.config4k.serializers.TemporalAmountSerializer
 import kotlinx.serialization.hocon.Hocon
+import kotlinx.serialization.hocon.serializers.ConfigMemorySizeSerializer
 import kotlinx.serialization.hocon.serializers.JavaDurationSerializer
 import kotlinx.serialization.modules.SerializersModule
 import kotlinx.serialization.modules.contextual
@@ -17,6 +18,7 @@ public val Config4kModule: SerializersModule =
         contextual(TemporalAmountSerializer)
         contextual(ConfigSerializer)
         contextual(ConfigValueSerializer)
+        contextual(ConfigMemorySizeSerializer)
     }
 
 public val Config4k: Hocon =


Test patch:
diff --git a/src/test/kotlin/io/github/config4k/serializers/ConfigMemorySizeSerializerTest.kt b/src/test/kotlin/io/github/config4k/serializers/ConfigMemorySizeSerializerTest.kt
new file mode 100644
index 0000000..1c2d0e7
--- /dev/null
+++ b/src/test/kotlin/io/github/config4k/serializers/ConfigMemorySizeSerializerTest.kt
@@ -0,0 +1,78 @@
+package io.github.config4k.serializers
+
+import com.typesafe.config.ConfigMemorySize
+import io.github.config4k.Config4k
+import io.github.config4k.render
+import io.github.config4k.toConfig
+import io.kotest.matchers.should
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.MissingFieldException
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.hocon.decodeFromConfig
+import kotlinx.serialization.hocon.encodeToConfig
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+class ConfigMemorySizeSerializerTest {
+    private companion object {
+        val OneMiB: ConfigMemorySize = ConfigMemorySize.ofBytes(1 * 1024 * 1024)
+        val TenKiB: ConfigMemorySize = ConfigMemorySize.ofBytes(10 * 1024)
+    }
+
+    @Nested
+    inner class SimpleTest {
+        @Test
+        fun checkDecoding() {
+            @Serializable
+            data class Conf(
+                @Contextual val size: ConfigMemorySize,
+            )
+
+            val test: Conf = Config4k.decodeFromConfig("size = 1 MiB".toConfig())
+            assertThat(test.size).isEqualTo(OneMiB)
+
+            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+                .should { assertThat(it.missingFields).contains("size") }
+        }
+
+        @Test
+        fun checkNullableDecoding() {
+            @Serializable
+            data class Conf(
+                @Contextual val size: ConfigMemorySize? = null,
+            )
+
+            val test: Conf = Config4k.decodeFromConfig("foo = bar".toConfig())
+            assertThat(test.size).isNull()
+        }
+
+        @Test
+        fun checkDefaultDecoding() {
+            @Serializable
+            data class Conf(
+                @Contextual val size: ConfigMemorySize = TenKiB,
+            )
+
+            val test: Conf = Config4k.decodeFromConfig("size = 1 MiB".toConfig())
+            assertThat(test.size).isEqualTo(OneMiB)
+
+            val default: Conf = Config4k.decodeFromConfig("foo = bar".toConfig())
+            assertThat(default.size).isEqualTo(TenKiB)
+        }
+
+        @Test
+        fun checkEncoding() {
+            @Serializable
+            data class Conf(
+                @Contextual val size: ConfigMemorySize,
+            )
+
+            var test = Config4k.encodeToConfig(Conf(OneMiB))
+            assertThat(test.render()).isEqualTo("size=\"1 MiB\"")
+            test = Config4k.encodeToConfig(Conf(TenKiB))
+            assertThat(test.render()).isEqualTo("size=\"10 KiB\"")
+        }
+    }
+}
diff --git a/src/test/kotlin/io/github/config4k/serializers/ConfigSerializerTest.kt b/src/test/kotlin/io/github/config4k/serializers/ConfigSerializerTest.kt
index 6a10628..3b1f0f6 100644
--- a/src/test/kotlin/io/github/config4k/serializers/ConfigSerializerTest.kt
+++ b/src/test/kotlin/io/github/config4k/serializers/ConfigSerializerTest.kt
@@ -8,8 +8,6 @@ import com.typesafe.config.ConfigValueType.OBJECT
 import io.github.config4k.Config4k
 import io.github.config4k.render
 import io.github.config4k.toConfig
-import io.kotest.matchers.should
-import io.kotest.matchers.throwable.shouldHaveMessage
 import io.mockk.mockk
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.MissingFieldException
@@ -18,9 +16,10 @@ import kotlinx.serialization.SerializationException
 import kotlinx.serialization.hocon.decodeFromConfig
 import kotlinx.serialization.hocon.encodeToConfig
 import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.assertj.core.api.Assertions.catchThrowableOfType
 import org.junit.jupiter.api.Nested
 import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
 
 class ConfigSerializerTest {
     @Nested
@@ -36,14 +35,15 @@ class ConfigSerializerTest {
             assertThat(test.conf).isNotNull()
             assertThat(test.conf.hasPath("foo")).isTrue()
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("conf") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("conf")
         }
 
         @Test
         fun checkIncorrectDecoder() {
-            assertThrows<SerializationException> { ConfigSerializer.deserialize(mockk()) }
-                .shouldHaveMessage("This class can be decoded only by Hocon format")
+            assertThatThrownBy { ConfigSerializer.deserialize(mockk()) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("This class can be decoded only by Hocon format")
         }
 
         @Test
@@ -89,8 +89,9 @@ class ConfigSerializerTest {
 
         @Test
         fun checkIncorrectEncoder() {
-            assertThrows<SerializationException> { ConfigSerializer.serialize(mockk(), mockk()) }
-                .shouldHaveMessage("This class can be encoded only by Hocon format")
+            assertThatThrownBy { ConfigSerializer.serialize(mockk(), mockk()) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("This class can be encoded only by Hocon format")
         }
     }
 
@@ -111,8 +112,8 @@ class ConfigSerializerTest {
             assertThat(test.conf[0].hasPath("foo")).isTrue()
             assertThat(test.conf[1].hasPath("fizz")).isTrue()
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("conf") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("conf")
         }
 
         @Test
@@ -189,8 +190,8 @@ class ConfigSerializerTest {
             assertThat(test.conf["key1"]?.hasPath("foo")).isTrue()
             assertThat(test.conf["key2"]?.hasPath("fizz")).isTrue()
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("conf") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("conf")
         }
 
         @Test
diff --git a/src/test/kotlin/io/github/config4k/serializers/ConfigValueSerializerTest.kt b/src/test/kotlin/io/github/config4k/serializers/ConfigValueSerializerTest.kt
index d07fb55..8cb2c06 100644
--- a/src/test/kotlin/io/github/config4k/serializers/ConfigValueSerializerTest.kt
+++ b/src/test/kotlin/io/github/config4k/serializers/ConfigValueSerializerTest.kt
@@ -10,8 +10,6 @@ import io.github.config4k.Config4k
 import io.github.config4k.render
 import io.github.config4k.toConfig
 import io.github.config4k.toConfigValue
-import io.kotest.matchers.should
-import io.kotest.matchers.throwable.shouldHaveMessage
 import io.mockk.mockk
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.MissingFieldException
@@ -20,10 +18,11 @@ import kotlinx.serialization.SerializationException
 import kotlinx.serialization.hocon.decodeFromConfig
 import kotlinx.serialization.hocon.encodeToConfig
 import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.assertj.core.api.Assertions.catchThrowableOfType
 import org.assertj.core.api.InstanceOfAssertFactories.MAP
 import org.junit.jupiter.api.Nested
 import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
 
 class ConfigValueSerializerTest {
     @Nested
@@ -39,14 +38,15 @@ class ConfigValueSerializerTest {
             assertThat(test.conf.valueType()).isEqualTo(OBJECT)
             assertThat(test.conf).asInstanceOf(MAP).containsKey("foo")
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("conf") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("conf")
         }
 
         @Test
         fun checkIncorrectDecoder() {
-            assertThrows<SerializationException> { ConfigValueSerializer.deserialize(mockk()) }
-                .shouldHaveMessage("This class can be decoded only by Hocon format")
+            assertThatThrownBy { ConfigValueSerializer.deserialize(mockk()) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("This class can be decoded only by Hocon format")
         }
 
         @Test
@@ -92,8 +92,9 @@ class ConfigValueSerializerTest {
 
         @Test
         fun checkIncorrectEncoder() {
-            assertThrows<SerializationException> { ConfigValueSerializer.serialize(mockk(), mockk()) }
-                .shouldHaveMessage("This class can be encoded only by Hocon format")
+            assertThatThrownBy { ConfigValueSerializer.serialize(mockk(), mockk()) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("This class can be encoded only by Hocon format")
         }
     }
 
@@ -116,8 +117,8 @@ class ConfigValueSerializerTest {
             assertThat(test.conf[1].valueType()).isEqualTo(OBJECT)
             assertThat(test.conf[1]).asInstanceOf(MAP).containsKey("fizz")
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("conf") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("conf")
         }
 
         @Test
@@ -206,8 +207,8 @@ class ConfigValueSerializerTest {
             assertThat(test.conf["key2"]?.valueType()).isEqualTo(OBJECT)
             assertThat(test.conf["key2"]).asInstanceOf(MAP).containsKey("fizz")
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("conf") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("conf")
         }
 
         @Test
diff --git a/src/test/kotlin/io/github/config4k/serializers/PeriodSerializerTest.kt b/src/test/kotlin/io/github/config4k/serializers/PeriodSerializerTest.kt
index e934243..c5d07f1 100644
--- a/src/test/kotlin/io/github/config4k/serializers/PeriodSerializerTest.kt
+++ b/src/test/kotlin/io/github/config4k/serializers/PeriodSerializerTest.kt
@@ -4,8 +4,6 @@ import com.typesafe.config.Config
 import io.github.config4k.Config4k
 import io.github.config4k.render
 import io.github.config4k.toConfig
-import io.kotest.matchers.should
-import io.kotest.matchers.throwable.shouldHaveMessage
 import io.mockk.mockk
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.MissingFieldException
@@ -14,9 +12,10 @@ import kotlinx.serialization.SerializationException
 import kotlinx.serialization.hocon.decodeFromConfig
 import kotlinx.serialization.hocon.encodeToConfig
 import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.assertj.core.api.Assertions.catchThrowableOfType
 import org.junit.jupiter.api.Nested
 import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
 import java.time.Period
 
 class PeriodSerializerTest {
@@ -37,14 +36,15 @@ class PeriodSerializerTest {
             val test: Conf = Config4k.decodeFromConfig("p = 1 y".toConfig())
             assertThat(test.p).isEqualTo(OneYear)
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("p") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("p")
         }
 
         @Test
         fun checkIncorrectDecoder() {
-            assertThrows<SerializationException> { PeriodSerializer.deserialize(mockk()) }
-                .shouldHaveMessage("This class can be decoded only by Hocon format")
+            assertThatThrownBy { PeriodSerializer.deserialize(mockk()) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("This class can be decoded only by Hocon format")
         }
 
         @Test
@@ -88,14 +88,16 @@ class PeriodSerializerTest {
             test = Config4k.encodeToConfig(Conf(Period.of(1, 20, 0)))
             assertThat(test.render()).isEqualTo("p=\"32 mo\"")
 
-            assertThrows<SerializationException> { Config4k.encodeToConfig(Conf(Period.of(1, 0, 2))) }
-                .shouldHaveMessage("java.time.Period can be specified by only one time unit")
+            assertThatThrownBy { Config4k.encodeToConfig(Conf(Period.of(1, 0, 2))) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("java.time.Period can be specified by only one time unit")
         }
 
         @Test
         fun checkIncorrectEncoder() {
-            assertThrows<SerializationException> { PeriodSerializer.serialize(mockk(), mockk()) }
-                .shouldHaveMessage("This class can be encoded only by Hocon format")
+            assertThatThrownBy { PeriodSerializer.serialize(mockk(), mockk()) }
+                .isInstanceOf(SerializationException::class.java)
+                .hasMessage("This class can be encoded only by Hocon format")
         }
     }
 
@@ -114,8 +116,8 @@ class PeriodSerializerTest {
             val test: Conf = Config4k.decodeFromConfig("ps = [10 d, 1 y]".toConfig())
             assertThat(test.ps).containsExactly(TenDays, OneYear)
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("ps") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("ps")
         }
 
         @Test
@@ -179,8 +181,8 @@ class PeriodSerializerTest {
                 .containsEntry("key1", OneYear)
                 .containsEntry("key2", TenDays)
 
-            assertThrows<MissingFieldException> { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
-                .should { assertThat(it.missingFields).contains("ps") }
+            val thrown = catchThrowableOfType(MissingFieldException::class.java) { Config4k.decodeFromConfig<Conf>("foo = bar".toConfig()) }
+            assertThat(thrown.missingFields).contains("ps")
         }
 
         @Test