Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,45 +198,42 @@ internal object Decoder {
else -> Utils.combine<Any?>(emptyList<Any?>(), leaf)
}
} else {
val mutableObj = LinkedHashMap<Any, Any?>(1)
// Always build *string-keyed* maps here
val mutableObj = LinkedHashMap<String, Any?>(1)

val cleanRoot =
if (root.startsWith("[") && root.endsWith("]")) {
root.substring(1, root.length - 1)
} else root

val decodedRoot =
if (options.getDecodeDotInKeys) {
cleanRoot.replace("%2E", ".")
} else cleanRoot
if (options.getDecodeDotInKeys) cleanRoot.replace("%2E", ".") else cleanRoot

val index: Int? =
if (decodedRoot.isNotEmpty() && decodedRoot.toIntOrNull() != null)
decodedRoot.toInt()
else null
val isPureNumeric = decodedRoot.isNotEmpty() && decodedRoot.all { it.isDigit() }
val idx: Int? = if (isPureNumeric) decodedRoot.toInt() else null
val isBracketedNumeric =
idx != null && root != decodedRoot && idx.toString() == decodedRoot

when {
!options.parseLists && decodedRoot == "" -> {
mutableObj[0] = leaf
// If list parsing is disabled OR listLimit < 0: always make a map with string
// key
!options.parseLists || options.listLimit < 0 -> {
val keyForMap = if (decodedRoot == "") "0" else decodedRoot
mutableObj[keyForMap] = leaf
obj = mutableObj
}

index != null &&
index >= 0 &&
root != decodedRoot &&
index.toString() == decodedRoot &&
options.parseLists &&
index <= options.listLimit -> {
val list = MutableList<Any?>(index + 1) { Undefined.Companion() }
list[index] = leaf
// Proper list index (e.g., "[3]") and allowed by listLimit -> build a list
isBracketedNumeric && idx >= 0 && idx <= options.listLimit -> {
val list = MutableList<Any?>(idx + 1) { Undefined.Companion() }
list[idx] = leaf
obj = list
}

// Otherwise, treat it as a map with *string* key (even if numeric)
else -> {
if (index != null) {
mutableObj[index] = leaf
} else {
mutableObj[decodedRoot] = leaf
}
val keyForMap = decodedRoot
mutableObj[keyForMap] = leaf
obj = mutableObj
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,12 @@ import io.github.techouse.qskotlin.enums.Format
import io.github.techouse.qskotlin.enums.Formatter
import io.github.techouse.qskotlin.enums.ListFormat
import io.github.techouse.qskotlin.enums.ListFormatGenerator
import io.github.techouse.qskotlin.models.DateSerializer
import io.github.techouse.qskotlin.models.Filter
import io.github.techouse.qskotlin.models.FunctionFilter
import io.github.techouse.qskotlin.models.IterableFilter
import io.github.techouse.qskotlin.models.Sorter
import io.github.techouse.qskotlin.models.Undefined
import io.github.techouse.qskotlin.models.ValueEncoder
import io.github.techouse.qskotlin.models.WeakWrapper
import io.github.techouse.qskotlin.models.*
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.time.LocalDateTime
import java.util.WeakHashMap
import kotlin.collections.get
import java.util.*

/** A helper object for encoding data into a query string format. */
internal object Encoder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,21 @@ internal object Utils {
is Iterable<*> ->
when {
target.any { it is Undefined } -> {
val mutableTarget: MutableMap<Any, Any?> =
target.withIndex().associate { it.index to it.value }.toMutableMap()
val mutableTarget: MutableMap<String, Any?> =
target
.withIndex()
.associate { it.index.toString() to it.value }
.toMutableMap()

when (source) {
is Iterable<*> ->
source.forEachIndexed { i, item ->
if (item !is Undefined) {
mutableTarget[i] = item
mutableTarget[i.toString()] = item
}
}

else -> mutableTarget[mutableTarget.size] = source
else -> mutableTarget[mutableTarget.size.toString()] = source
}

when {
Expand Down Expand Up @@ -117,7 +120,7 @@ internal object Utils {
is Iterable<*> -> {
source.forEachIndexed { i, item ->
if (item !is Undefined) {
mutableTarget[i] = item
mutableTarget[i.toString()] = item
}
}
}
Expand Down Expand Up @@ -146,16 +149,16 @@ internal object Utils {
if (target == null || target !is Map<*, *>) {
return when (target) {
is Iterable<*> -> {
val mutableTarget: MutableMap<Any, Any?> =
val mutableTarget: MutableMap<String, Any?> =
target
.withIndex()
.associate { it.index to it.value }
.associate { it.index.toString() to it.value }
.filterValues { it !is Undefined }
.toMutableMap()

@Suppress("UNCHECKED_CAST")
(source as Map<Any, Any?>).forEach { (key, value) ->
mutableTarget[key] = value
mutableTarget[key.toString()] = value
}
mutableTarget
}
Expand Down Expand Up @@ -183,10 +186,9 @@ internal object Utils {
target is Iterable<*> && source !is Iterable<*> ->
target
.withIndex()
.associate { it.index to it.value }
.associate { it.index.toString() to it.value }
.filterValues { it !is Undefined }
.toMutableMap()

else -> (target as Map<Any, Any?>).toMutableMap()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,12 @@ internal val EmptyTestCases: List<Map<String, Any>> =
"indices" to "[0]=a&[1]=b& [0]=1",
"repeat" to "=a&=b& =1",
),
"noEmptyKeys" to mapOf(0 to "a", 1 to "b", " " to listOf("1")),
"noEmptyKeys" to mapOf("0" to "a", "1" to "b", " " to listOf("1")),
),
mapOf(
"input" to "[0]=a&[1]=b&a[0]=1&a[1]=2",
"withEmptyKeys" to mapOf("" to listOf("a", "b"), "a" to listOf("1", "2")),
"noEmptyKeys" to mapOf(0 to "a", 1 to "b", "a" to listOf("1", "2")),
"noEmptyKeys" to mapOf("0" to "a", "1" to "b", "a" to listOf("1", "2")),
"stringifyOutput" to
mapOf(
"brackets" to "[]=a&[]=b&a[]=1&a[]=2",
Expand All @@ -207,6 +207,6 @@ internal val EmptyTestCases: List<Map<String, Any>> =
"withEmptyKeys" to mapOf("" to listOf("a", "b")),
"stringifyOutput" to
mapOf("brackets" to "[]=a&[]=b", "indices" to "[0]=a&[1]=b", "repeat" to "=a&=b"),
"noEmptyKeys" to mapOf(0 to "a", 1 to "b"),
"noEmptyKeys" to mapOf("0" to "a", "1" to "b"),
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class DecodeSpec :
}

it("parses a simple string") {
decode("0=foo") shouldBe mapOf(0 to "foo")
decode("0=foo") shouldBe mapOf("0" to "foo")
decode("foo=c++") shouldBe mapOf("foo" to "c ")
decode("a[>=]=23") shouldBe mapOf("a" to mapOf(">=" to "23"))
decode("a[<=>]==23") shouldBe mapOf("a" to mapOf("<=>" to "=23"))
Expand Down Expand Up @@ -304,25 +304,25 @@ class DecodeSpec :
decode("a[1]=c&a[0]=b") shouldBe mapOf("a" to listOf("b", "c"))
decode("a[1]=c", DecodeOptions(listLimit = 20)) shouldBe mapOf("a" to listOf("c"))
decode("a[1]=c", DecodeOptions(listLimit = 0)) shouldBe
mapOf("a" to mapOf(1 to "c"))
mapOf("a" to mapOf("1" to "c"))
decode("a[1]=c") shouldBe mapOf("a" to listOf("c"))
decode("a[0]=b&a[2]=c", DecodeOptions(parseLists = false)) shouldBe
mapOf("a" to mapOf(0 to "b", 2 to "c"))
mapOf("a" to mapOf("0" to "b", "2" to "c"))
decode("a[0]=b&a[2]=c", DecodeOptions(parseLists = true)) shouldBe
mapOf("a" to listOf("b", "c"))
decode("a[1]=b&a[15]=c", DecodeOptions(parseLists = false)) shouldBe
mapOf("a" to mapOf(1 to "b", 15 to "c"))
mapOf("a" to mapOf("1" to "b", "15" to "c"))
decode("a[1]=b&a[15]=c", DecodeOptions(parseLists = true)) shouldBe
mapOf("a" to listOf("b", "c"))
}

it("limits specific list indices to listLimit") {
decode("a[20]=a", DecodeOptions(listLimit = 20)) shouldBe mapOf("a" to listOf("a"))
decode("a[21]=a", DecodeOptions(listLimit = 20)) shouldBe
mapOf("a" to mapOf(21 to "a"))
mapOf("a" to mapOf("21" to "a"))

decode("a[20]=a") shouldBe mapOf("a" to listOf("a"))
decode("a[21]=a") shouldBe mapOf("a" to mapOf(21 to "a"))
decode("a[21]=a") shouldBe mapOf("a" to mapOf("21" to "a"))
}

it("supports keys that begin with a number") {
Expand Down Expand Up @@ -351,15 +351,15 @@ class DecodeSpec :

it("transforms lists to maps") {
decode("foo[0]=bar&foo[bad]=baz") shouldBe
mapOf("foo" to mapOf(0 to "bar", "bad" to "baz"))
mapOf("foo" to mapOf("0" to "bar", "bad" to "baz"))
decode("foo[bad]=baz&foo[0]=bar") shouldBe
mapOf("foo" to mapOf("bad" to "baz", 0 to "bar"))
mapOf("foo" to mapOf("bad" to "baz", "0" to "bar"))
decode("foo[bad]=baz&foo[]=bar") shouldBe
mapOf("foo" to mapOf("bad" to "baz", 0 to "bar"))
mapOf("foo" to mapOf("bad" to "baz", "0" to "bar"))
decode("foo[]=bar&foo[bad]=baz") shouldBe
mapOf("foo" to mapOf(0 to "bar", "bad" to "baz"))
mapOf("foo" to mapOf("0" to "bar", "bad" to "baz"))
decode("foo[bad]=baz&foo[]=bar&foo[]=foo") shouldBe
mapOf("foo" to mapOf("bad" to "baz", 0 to "bar", 1 to "foo"))
mapOf("foo" to mapOf("bad" to "baz", "0" to "bar", "1" to "foo"))
decode("foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb") shouldBe
mapOf(
"foo" to
Expand Down Expand Up @@ -387,13 +387,13 @@ class DecodeSpec :
DecodeOptions(allowDots = true),
) shouldBe mapOf("foo" to listOf(mapOf("baz" to listOf("15", "16"), "bar" to "2")))
decode("foo.bad=baz&foo[0]=bar", DecodeOptions(allowDots = true)) shouldBe
mapOf("foo" to mapOf("bad" to "baz", 0 to "bar"))
mapOf("foo" to mapOf("bad" to "baz", "0" to "bar"))
decode("foo.bad=baz&foo[]=bar", DecodeOptions(allowDots = true)) shouldBe
mapOf("foo" to mapOf("bad" to "baz", 0 to "bar"))
mapOf("foo" to mapOf("bad" to "baz", "0" to "bar"))
decode("foo[]=bar&foo.bad=baz", DecodeOptions(allowDots = true)) shouldBe
mapOf("foo" to mapOf(0 to "bar", "bad" to "baz"))
mapOf("foo" to mapOf("0" to "bar", "bad" to "baz"))
decode("foo.bad=baz&foo[]=bar&foo[]=foo", DecodeOptions(allowDots = true)) shouldBe
mapOf("foo" to mapOf("bad" to "baz", 0 to "bar", 1 to "foo"))
mapOf("foo" to mapOf("bad" to "baz", "0" to "bar", "1" to "foo"))
decode(
"foo[0].a=a&foo[0].b=b&foo[1].a=aa&foo[1].b=bb",
DecodeOptions(allowDots = true),
Expand All @@ -406,7 +406,7 @@ class DecodeSpec :

it("correctly prunes undefined values when converting a list to a map") {
decode("a[2]=b&a[99999999]=c") shouldBe
mapOf("a" to mapOf(2 to "b", 99999999 to "c"))
mapOf("a" to mapOf("2" to "b", "99999999" to "c"))
}

it("supports malformed uri characters") {
Expand Down Expand Up @@ -482,9 +482,9 @@ class DecodeSpec :
}

it("continues parsing when no parent is found") {
decode("[]=&a=b") shouldBe mapOf(0 to "", "a" to "b")
decode("[]=&a=b") shouldBe mapOf("0" to "", "a" to "b")
decode("[]&a=b", DecodeOptions(strictNullHandling = true)) shouldBe
mapOf(0 to null, "a" to "b")
mapOf("0" to null, "a" to "b")
decode("[foo]=bar") shouldBe mapOf("foo" to "bar")
}

Expand Down Expand Up @@ -519,25 +519,25 @@ class DecodeSpec :

it("allows overriding list limit") {
decode("a[0]=b", DecodeOptions(listLimit = -1)) shouldBe
mapOf("a" to mapOf(0 to "b"))
mapOf("a" to mapOf("0" to "b"))
decode("a[0]=b", DecodeOptions(listLimit = 0)) shouldBe mapOf("a" to listOf("b"))

decode("a[-1]=b", DecodeOptions(listLimit = -1)) shouldBe
mapOf("a" to mapOf(-1 to "b"))
mapOf("a" to mapOf("-1" to "b"))
decode("a[-1]=b", DecodeOptions(listLimit = 0)) shouldBe
mapOf("a" to mapOf(-1 to "b"))
mapOf("a" to mapOf("-1" to "b"))

decode("a[0]=b&a[1]=c", DecodeOptions(listLimit = -1)) shouldBe
mapOf("a" to mapOf(0 to "b", 1 to "c"))
mapOf("a" to mapOf("0" to "b", "1" to "c"))
decode("a[0]=b&a[1]=c", DecodeOptions(listLimit = 0)) shouldBe
mapOf("a" to mapOf(0 to "b", 1 to "c"))
mapOf("a" to mapOf("0" to "b", "1" to "c"))
}

it("allows disabling list parsing") {
decode("a[0]=b&a[1]=c", DecodeOptions(parseLists = false)) shouldBe
mapOf("a" to mapOf(0 to "b", 1 to "c"))
mapOf("a" to mapOf("0" to "b", "1" to "c"))
decode("a[]=b", DecodeOptions(parseLists = false)) shouldBe
mapOf("a" to mapOf(0 to "b"))
mapOf("a" to mapOf("0" to "b"))
}

it("allows for query string prefix") {
Expand Down Expand Up @@ -725,7 +725,7 @@ class DecodeSpec :

val expectedList = mutableMapOf<Any, Any?>()
expectedList["a"] = mutableMapOf<Any, Any?>()
(expectedList["a"] as MutableMap<Any, Any?>)[0] = "b"
(expectedList["a"] as MutableMap<Any, Any?>)["0"] = "b"
(expectedList["a"] as MutableMap<Any, Any?>)["c"] = "d"
decode("a[]=b&a[c]=d") shouldBe expectedList
}
Expand Down Expand Up @@ -1032,7 +1032,17 @@ class DecodeSpec :
"a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6",
DecodeOptions(listLimit = 5),
) shouldBe
mapOf("a" to mapOf(1 to "1", 2 to "2", 3 to "3", 4 to "4", 5 to "5", 6 to "6"))
mapOf(
"a" to
mapOf(
"1" to "1",
"2" to "2",
"3" to "3",
"4" to "4",
"5" to "5",
"6" to "6",
)
)
}

it("handles list limit of zero correctly") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,21 @@ class ExampleSpec :
}

it("converts high indices to Map keys") {
decode("a[100]=b") shouldBe mapOf("a" to mapOf(100 to "b"))
decode("a[100]=b") shouldBe mapOf("a" to mapOf("100" to "b"))
}

it("can override list limit") {
decode("a[1]=b", DecodeOptions(listLimit = 0)) shouldBe
mapOf("a" to mapOf(1 to "b"))
mapOf("a" to mapOf("1" to "b"))
}

it("can disable list parsing entirely") {
decode("a[]=b", DecodeOptions(parseLists = false)) shouldBe
mapOf("a" to mapOf(0 to "b"))
mapOf("a" to mapOf("0" to "b"))
}

it("merges mixed notations into Map") {
decode("a[0]=b&a[b]=c") shouldBe mapOf("a" to mapOf(0 to "b", "b" to "c"))
decode("a[0]=b&a[b]=c") shouldBe mapOf("a" to mapOf("0" to "b", "b" to "c"))
}

it("can create lists of Maps") {
Expand Down
Loading
Loading