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 @@ -12,6 +12,7 @@
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import kotlin.text.RegexOption;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -154,4 +155,11 @@ void regexFactoryOverloadsAndSplit() {
assertTrue(ts.contains("abc"));
assertTrue(ts.contains(String.valueOf(ciUnicode.getFlags())));
}

@Test
@DisplayName("regex(pattern, Set<RegexOption>) factory works for Java callers")
void regexFactoryWithKotlinOptions() {
RegexDelimiter fromOptions = Delimiter.regex("[,&]", java.util.Set.of(RegexOption.IGNORE_CASE));
assertTrue((fromOptions.getFlags() & Pattern.CASE_INSENSITIVE) != 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ void map_toQueryString_encodes_all_fixtures() {
assertEquals(tc.getEncoded(), qs, "encode mismatch for: " + input);
}
}

@Test
void map_toQueryString_uses_default_overload() {
assertEquals("a=b", QS.toQueryString(Map.of("a", "b")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ class ExtensionsSpec :
testCase.encoded
}
}

test("uses default EncodeOptions overload when omitted") {
mapOf("a" to "b").toQueryString() shouldBe "a=b"
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class SentinelSpec :
FunSpec({
test("sentinel encoded values are exposed") {
Sentinel.ISO.asQueryParam() shouldBe Sentinel.ISO.toString()
Sentinel.ISO.encoded shouldBe "utf8=%26%2310003%3B"
Sentinel.CHARSET.toEntry().key shouldBe Sentinel.PARAM_NAME
Sentinel.CHARSET.encoded shouldBe "utf8=%E2%9C%93"
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ class UtilsSpec :
Utils.decode("name%2Eobj%2Efoo", StandardCharsets.ISO_8859_1) shouldBe
"name.obj.foo"
}

test("returns input unchanged when URLDecoder cannot parse UTF-8 sequence") {
Utils.decode("%E0%") shouldBe "%E0%"
}

test("handles null input safely") { Utils.decode(null) shouldBe null }
}

context("Utils.compact") {
Expand All @@ -228,6 +234,13 @@ class UtilsSpec :
val compacted = Utils.compact(root, allowSparseLists = true)
compacted["self"].shouldBeInstanceOf<MutableList<*>>()
}

test("default allowSparseLists removes undefined entries") {
val list = mutableListOf<Any?>(Undefined(), "ok")
val root: MutableMap<String, Any?> = mutableMapOf("items" to list)

Utils.compact(root)["items"] shouldBe mutableListOf("ok")
}
}

@Suppress("DEPRECATION")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.github.techouse.qskotlin.unit.internal

import io.github.techouse.qskotlin.enums.Duplicates
import io.github.techouse.qskotlin.internal.Decoder
import io.github.techouse.qskotlin.models.DecodeOptions
import io.github.techouse.qskotlin.models.Undefined
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
Expand Down Expand Up @@ -43,5 +45,107 @@ class DecoderInternalSpec :
result.containsKey("name") shouldBe true
result["name"] shouldBe "A"
}

it("uses default options overload when omitted") {
Decoder.parseQueryStringValues("foo=bar") shouldBe mutableMapOf("foo" to "bar")
}

it("decodes bare keys honoring strictNullHandling") {
val result =
Decoder.parseQueryStringValues("flag", DecodeOptions(strictNullHandling = true))

result shouldBe mutableMapOf<String, Any?>("flag" to null)
}

it("wraps bracket suffix comma values as nested lists") {
val options = DecodeOptions(comma = true)
val result = Decoder.parseQueryStringValues("tags[]=a,b", options)

result["tags[]"] shouldBe listOf(listOf("a", "b"))
}

it("combines duplicate keys when duplicates=COMBINE") {
val options = DecodeOptions(duplicates = Duplicates.COMBINE)
val result = Decoder.parseQueryStringValues("k=1&k=2", options)

result["k"] shouldBe listOf("1", "2")
}
}

describe("Decoder.parseKeys") {
it("handles nested list chains while respecting parent indices") {
val options = DecodeOptions(parseLists = true)
val value = listOf(listOf("x", "y"))

@Suppress("UNCHECKED_CAST")
val parsed =
Decoder.parseKeys(
givenKey = "0[]",
value = value,
options = options,
valuesParsed = true,
) as Map<String, Any?>

parsed["0"] shouldBe listOf(listOf("x", "y"))
}

it("produces empty list when allowEmptyLists consumes blank value") {
val options =
DecodeOptions(
parseLists = true,
allowEmptyLists = true,
strictNullHandling = false,
)

val parsed =
Decoder.parseKeys(
givenKey = "list[]",
value = "",
options = options,
valuesParsed = true,
) as Map<*, *>

parsed["list"] shouldBe mutableListOf<Any?>()
}

it("falls back to map entries when parseLists disabled") {
val options = DecodeOptions(parseLists = false)

val parsed =
Decoder.parseKeys(
givenKey = "arr[3]",
value = "v",
options = options,
valuesParsed = true,
) as Map<*, *>

parsed["arr"] shouldBe mapOf("3" to "v")
}

it("enforces listLimit for nested list growth") {
val options = DecodeOptions(listLimit = 1, throwOnLimitExceeded = true)
val nested = listOf(listOf("a", "b"))

shouldThrow<IndexOutOfBoundsException> {
Decoder.parseKeys(
givenKey = "0[]",
value = nested,
options = options,
valuesParsed = false,
)
}
}

it("creates indexed lists for bracketed numeric keys within limit") {
val parsed =
Decoder.parseKeys(
givenKey = "items[2]",
value = "x",
options = DecodeOptions(),
valuesParsed = true,
) as Map<*, *>

parsed["items"] shouldBe mutableListOf(Undefined(), Undefined(), "x")
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import io.github.techouse.qskotlin.enums.Format
import io.github.techouse.qskotlin.enums.ListFormat
import io.github.techouse.qskotlin.internal.Encoder
import io.github.techouse.qskotlin.models.IterableFilter
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.time.LocalDateTime

class EncoderInternalSpec :
Expand Down Expand Up @@ -136,6 +138,77 @@ class EncoderInternalSpec :
seenValues shouldBe listOf("alpha")
result shouldBe listOf("tags[]=enc:alpha")
}

it("returns empty suffix when allowEmptyLists enabled for empty iterable") {
val result =
Encoder.encode(
data = emptyList<Any?>(),
undefined = false,
sideChannel = mutableMapOf(),
prefix = "items",
allowEmptyLists = true,
)

result shouldBe "items[]"
}

it("stringifies temporal comma lists when no serializer supplied") {
val instant = Instant.parse("2020-01-01T00:00:00Z")
val date = LocalDateTime.parse("2020-01-01T00:00:00")

val result =
Encoder.encode(
data = listOf(instant, date),
undefined = false,
sideChannel = mutableMapOf(),
prefix = "ts",
generateArrayPrefix = ListFormat.COMMA.generator,
encoder = { value, _, _ -> value?.toString() ?: "" },
)

result shouldBe listOf("ts=2020-01-01T00:00:00Z,2020-01-01T00:00")
}

it("serializes LocalDateTime with default ISO formatting when serializer null") {
val stamp = LocalDateTime.parse("2024-01-02T03:04:05")

val result =
Encoder.encode(
data = stamp,
undefined = false,
sideChannel = mutableMapOf(),
prefix = "ts",
)

result shouldBe "ts=2024-01-02T03:04:05"
}

it("propagates undefined flag by returning empty mutable list") {
val result =
Encoder.encode(
data = null,
undefined = true,
sideChannel = mutableMapOf(),
prefix = "ignored",
)

result.shouldBeInstanceOf<MutableList<*>>().isEmpty() shouldBe true
}

it("detects cyclic references and throws") {
val cycle = mutableMapOf<String, Any?>()
cycle["self"] = cycle

shouldThrow<IndexOutOfBoundsException> {
Encoder.encode(
data = cycle,
undefined = false,
sideChannel = mutableMapOf(),
prefix = "self",
)
}
.message shouldBe "Cyclic object value"
}
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ class DecodeOptionsSpec :
newOptions.parseLists shouldBe true
newOptions.strictNullHandling shouldBe false
}

it("rejects negative depth at construction time") {
shouldThrow<IllegalArgumentException> { DecodeOptions(depth = -1) }
}

it("exposes defaults() convenience instance") {
DecodeOptions.defaults() shouldBe DecodeOptions()
}
}

val charsets = listOf(StandardCharsets.UTF_8, StandardCharsets.ISO_8859_1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class DelimiterSpec :
}
fromEmptyOptions.shouldBeInstanceOf<RegexDelimiter>().flags shouldBe 0

Delimiter.regex(pattern = "[;&]", options = setOf(RegexOption.LITERAL))
.shouldBeInstanceOf<RegexDelimiter>()

fromFlags.split("a=b;c=d").filter { it.isNotEmpty() } shouldBe listOf("a=b", "c=d")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ class WeakWrapperSpec :
w1 shouldNotBe Any()
}

it("equals short-circuits when other referent already cleared") {
val referent = Any()
val w1 = WeakWrapper(referent)
val w2 = WeakWrapper(referent)
val field =
WeakWrapper::class.java.getDeclaredField("weakRef").apply {
isAccessible = true
}
field.set(w2, WeakReference<Any?>(null))

(w1 == w2) shouldBe false
}

it("Inequality: different referents") {
val w1 = WeakWrapper(Any())
val w2 = WeakWrapper(Any())
Expand Down
Loading