Repository: config4k/config4k
Pull request #106 (2021-03-12 11:33:05)

Added default value at extension Config.extract

CrazyMoonkin: Sorry, I forgot update Readme.md.
ihostage: > Sorry, I forgot update Readme.md.

It's not a problem. You can do that at any time 😉  I don't know when will find time for review yet. 🤷‍♂️ 


codecov[bot]: # [Codecov](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=h1) Report
> Merging [#106](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=desc) (1377dfd) into [master](https://codecov.io/gh/config4k/config4k/commit/ccde51af8d55bc2f24e87dec65059e0fdba97ac6?el=desc) (ccde51a) will **decrease** coverage by `3.15%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/config4k/config4k/pull/106/graphs/tree.svg?width=650&height=150&src=pr&token=RWhHoqiMra)](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master     #106      +/-   ##
============================================
- Coverage     83.79%   80.64%   -3.16%     
  Complexity       56       56              
============================================
  Files            30       30              
  Lines           179      186       +7     
  Branches         76       78       +2     
============================================
  Hits            150      150              
- Misses           10       17       +7     
  Partials         19       19              
```


| [Impacted Files](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [src/main/kotlin/io/github/config4k/Extension.kt](https://codecov.io/gh/config4k/config4k/pull/106/diff?src=pr&el=tree#diff-c3JjL21haW4va290bGluL2lvL2dpdGh1Yi9jb25maWc0ay9FeHRlbnNpb24ua3Q=) | `60.52% <0.00%> (-13.67%)` | `0.00 <0.00> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=footer). Last update [ccde51a...1377dfd](https://codecov.io/gh/config4k/config4k/pull/106?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

CrazyMoonkin: > 
> 
> Sorry, I forgot update Readme.md.

Fixed.
CrazyMoonkin: I added 4 words to code and 4 test for check it. I don't know why the coverage decreased:(
CrazyMoonkin: Hello! Could you review this pr.
CrazyMoonkin: @ihostage 
ihostage: Hi, @CrazyMoonkin! Why do you want to extend library API instead of using a [Kotlin Elvis](https://kotlinlang.org/docs/null-safety.html#elvis-operator) directly as an idiomatic way for this case on the language level?
CrazyMoonkin: @ihostage I think that library API must support default value because it wiil be more user frendly. 

```kotlin
  val val1 = config.extract("value", 2)
  val val2 = config.extract("value", "some string")
  val val3 = config.extract("value", true)

  val val5 = config.extract("value") ?: 1
  val val6 = config.extract("value") ?: "some string"
  val val7 = config.extract("value") ?: true
```

P.S. for example spring @Value annotation providing default value.

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9a7af49..6bf5c16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,4 +21,4 @@ bundle install
 rm -rf _site/ && bundle exec jekyll serve -H 0.0.0.0 --incremental
 ```
 
-Open [http://localhost:4000/config4k](http://localhost:4000/config4k) in your browser.
+Open [http://localhost:4000/config4k](http://localhost:4000/config4k/) in your browser.
diff --git a/docs/usage/deserialization/default-values.md b/docs/usage/deserialization/default-values.md
new file mode 100644
index 0000000..4ec1a3d
--- /dev/null
+++ b/docs/usage/deserialization/default-values.md
@@ -0,0 +1,16 @@
+---
+layout: default
+title: Default values
+permalink: /usage/deserialization/default-values
+snippet_source: "../src/test/kotlin/io/github/config4k/DocExamples.kt"
+snippet_comment_prefix: "//"
+parent: Deserialization
+grand_parent: Usage
+nav_order: 4
+---
+
+Config4k also supports default values.
+`extract<T, T?>` method returns `T` when the path exists and default value when it does not exist.
+```kotlin
+{% include_snippet deserialization-default-values %}
+```
diff --git a/docs/usage/deserialization/enum.md b/docs/usage/deserialization/enum.md
index b579658..63b462e 100644
--- a/docs/usage/deserialization/enum.md
+++ b/docs/usage/deserialization/enum.md
@@ -6,7 +6,7 @@ snippet_source: "../src/test/kotlin/io/github/config4k/DocExamples.kt"
 snippet_comment_prefix: "//"
 parent: Deserialization
 grand_parent: Usage
-nav_order: 4
+nav_order: 5
 ---
 
 Config4k also supports Enum. Enum is converted to String of its name in the config file.
diff --git a/src/main/kotlin/io/github/config4k/Extension.kt b/src/main/kotlin/io/github/config4k/Extension.kt
index 89256c7..b5c9bcc 100644
--- a/src/main/kotlin/io/github/config4k/Extension.kt
+++ b/src/main/kotlin/io/github/config4k/Extension.kt
@@ -21,11 +21,12 @@ import kotlin.reflect.full.primaryConstructor
  * As this function is an inline function, shown stacktrace is not true.
  *
  * @param path see [com.typesafe.config.Config]
+ * @param defaultValue will be return if Config doesn't contain value by path
  */
-public inline fun <reified T> Config.extract(path: String): T {
+public inline fun <reified T> Config.extract(path: String, defaultValue: T? = null): T {
     val genericType = object : TypeReference<T>() {}.genericType()
 
-    val result = SelectReader.getReader(ClassContainer(T::class, genericType))(this, path)
+    val result = SelectReader.getReader(ClassContainer(T::class, genericType))(this, path) ?: defaultValue
 
     return try {
         result as T


Test patch:
diff --git a/src/test/kotlin/io/github/config4k/DocExamples.kt b/src/test/kotlin/io/github/config4k/DocExamples.kt
index b33e419..8d5f8ea 100644
--- a/src/test/kotlin/io/github/config4k/DocExamples.kt
+++ b/src/test/kotlin/io/github/config4k/DocExamples.kt
@@ -13,6 +13,7 @@ fun main() {
     DocExamples.serializeString()
     DocExamples.serializeHocon()
     DocExamples.serializeWithoutComments()
+    DocExamples.extractDefaultValues()
 }
 
 object DocExamples {
@@ -150,4 +151,12 @@ object DocExamples {
         println(person.root().render(options))
         // end-snippet
     }
+
+    fun extractDefaultValues() {
+        // begin-snippet: deserialization-default-values
+        val config = ConfigFactory.parseString("""key = 10""")
+        val key = config.extract("unknown", 20)
+        println(key) // 20
+        // end-snippet
+    }
 }
diff --git a/src/test/kotlin/io/github/config4k/TestConfig4kException.kt b/src/test/kotlin/io/github/config4k/TestConfig4kException.kt
index b7040f4..d4a8bc9 100644
--- a/src/test/kotlin/io/github/config4k/TestConfig4kException.kt
+++ b/src/test/kotlin/io/github/config4k/TestConfig4kException.kt
@@ -28,6 +28,13 @@ class TestConfig4kException : WordSpec({
                 config.extract<Size>("key")
             }.message shouldBe "expected : [SMALL, MEDIUM, LARGE], actually : foo"
         }
+
+        "throw ConfigException.BadPath if default value is null with not nullable type" {
+            val config = ConfigFactory.empty()
+            shouldThrow<ConfigException.BadPath> {
+                config.extract<Int>("value", null)
+            }.message shouldBe "Invalid path 'value': take a look at your config"
+        }
     }
 })
 
diff --git a/src/test/kotlin/io/github/config4k/TestExtension.kt b/src/test/kotlin/io/github/config4k/TestExtension.kt
index 82b096e..dd07dc7 100644
--- a/src/test/kotlin/io/github/config4k/TestExtension.kt
+++ b/src/test/kotlin/io/github/config4k/TestExtension.kt
@@ -36,13 +36,13 @@ class TestExtension : WordSpec({
         "return Double value" {
             val num = 0.1
             val config = ConfigFactory.parseString("""value = $num""")
-            config.extract<Double>("value") shouldBeExactly (num)
+            config.extract<Double>("value") shouldBeExactly num
         }
 
         "return Float value" {
             val num = 0.1f
             val config = ConfigFactory.parseString("""value = $num""")
-            config.extract<Float>("value") shouldBeExactly (num)
+            config.extract<Float>("value") shouldBeExactly num
         }
 
         "return Long value" {
@@ -54,22 +54,19 @@ class TestExtension : WordSpec({
         "return ConfigMemorySize" {
             val memorySize = "100KiB"
             val config = ConfigFactory.parseString("""value = $memorySize""")
-            config.extract<ConfigMemorySize>("value") shouldBe
-                ConfigMemorySize.ofBytes(100 * 1024)
+            config.extract<ConfigMemorySize>("value") shouldBe ConfigMemorySize.ofBytes(100 * 1024)
         }
 
         "return Duration" {
             val duration = "60minutes"
             val config = ConfigFactory.parseString("""value = $duration""")
-            config.extract<Duration>("value") shouldBe
-                Duration.ofMinutes(60)
+            config.extract<Duration>("value") shouldBe Duration.ofMinutes(60)
         }
 
         "return Period" {
             val period = "10years"
             val config = ConfigFactory.parseString("""value = $period""")
-            config.extract<Period>("value") shouldBe
-                Period.ofYears(10)
+            config.extract<Period>("value") shouldBe Period.ofYears(10)
         }
 
         "return Regex" {
@@ -81,8 +78,7 @@ class TestExtension : WordSpec({
         "return TemporalAmount" {
             val temporalAmount = "5weeks"
             val config = ConfigFactory.parseString("""value = $temporalAmount""")
-            config.extract<TemporalAmount>("value") shouldBe
-                Period.ofWeeks(5)
+            config.extract<TemporalAmount>("value") shouldBe Period.ofWeeks(5)
         }
 
         "return Config" {
@@ -107,6 +103,18 @@ class TestExtension : WordSpec({
             configValue.valueType() shouldBe ConfigValueType.BOOLEAN
             configValue.unwrapped() shouldBe b
         }
+
+        "return not default Int value" {
+            val num = 10
+            val config = ConfigFactory.parseString("""value = $num""")
+            config.extract("value", 1) shouldBe num
+        }
+
+        "return default Int value" {
+            val num = 0
+            val config = ConfigFactory.parseString("""value = 1""")
+            config.extract("value1", num) shouldBe num
+        }
     }
     "Config property delegate" should {
         "return Int value" {
diff --git a/src/test/kotlin/io/github/config4k/TestNullable.kt b/src/test/kotlin/io/github/config4k/TestNullable.kt
index 33c0b96..883831d 100644
--- a/src/test/kotlin/io/github/config4k/TestNullable.kt
+++ b/src/test/kotlin/io/github/config4k/TestNullable.kt
@@ -16,6 +16,11 @@ class TestNullable : WordSpec({
             val config = ConfigFactory.empty()
             config.extract<Int?>("key") shouldBe null
         }
+
+        "return null with default value" {
+            val config = ConfigFactory.empty()
+            config.extract<Int?>("key", null) shouldBe null
+        }
     }
 
     "Any.toConfig" should {