![JAR search and dependency download from the Maven repository](/logo.png)
test.kotlin.com.amazon.ionpathextraction.PathExtractorTest.kt Maven / Gradle / Ivy
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at:
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package com.amazon.ionpathextraction
import com.amazon.ion.*
import com.amazon.ion.system.*
import com.amazon.ionpathextraction.exceptions.PathExtractionException
import com.amazon.ionpathextraction.pathcomponents.PathComponent
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.ValueSource
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.stream.Stream
import kotlin.test.assertTrue
abstract class PathExtractorTest {
companion object {
private val ION = IonSystemBuilder.standard().build()
data class TestCase(val searchPaths: List,
val data: String,
val expected: IonList,
val stepOutNumber: Int,
val hasMultipleTopLevelValues: Boolean,
val legacyOnly: Boolean = false,
val caseInsensitive: String = "None") {
override fun toString(): String = "SearchPaths=$searchPaths, " +
"Data=$data, " +
"Expected=$expected, " +
"StepOutN=$stepOutNumber" +
"Legacy=$legacyOnly" +
"CaseInsensitive=$caseInsensitive"
}
private fun IonValue.toText(): String {
val out = StringBuilder()
ION.newTextWriter(out).use { writer ->
if (hasTypeAnnotation("${'$'}datagram") && this is IonContainer) {
forEach { it -> it.writeTo(writer) }
} else {
this.writeTo(writer)
}
}
return out.toString()
}
@JvmStatic
fun testCases(): Stream =
ION.loader.load(File("src/test/resources/test-cases.ion"))
.map { it as IonStruct }
.map { struct ->
// single
val searchPaths = if (struct.containsKey("searchPath")) {
listOf(struct["searchPath"].toText())
}
// multiple
else {
(struct["searchPaths"] as IonSequence).map { it.toText() }
}
TestCase(
searchPaths,
struct["data"].toText(),
struct["expected"] as IonList,
struct["stepOutN"]?.let { (it as IonInt).intValue() } ?: 0,
struct["data"].hasTypeAnnotation("${'$'}datagram"),
struct.hasTypeAnnotation("legacy"),
struct.get("caseInsensitive")?.toText() ?: "None"
)
}.stream()
enum class API {
MATCH {
override fun match(extractor: PathExtractor, reader: IonReader, context: T?) {
extractor.match(reader, context)
}
},
MATCH_CURRENT_VALUE {
override fun match(extractor: PathExtractor, reader: IonReader, context: T?) {
reader.next()
extractor.matchCurrentValue(reader, context)
}
};
abstract fun match(extractor: PathExtractor, reader: IonReader, context: T? = null)
}
}
abstract fun PathExtractorBuilder.buildExtractor(): PathExtractor
private val emptyCallback: (IonReader) -> Int = { 0 }
private fun collectToIonList(stepOutN: Int): (IonReader, IonList) -> Int = { reader, out ->
ION.newWriter(out).use { it.writeValue(reader) }
stepOutN
}
@ParameterizedTest
@MethodSource("testCases")
open fun testSearchPaths(testCase: TestCase) {
val builder = PathExtractorBuilder.standard()
testCase.searchPaths.forEach { builder.withSearchPath(it, collectToIonList(testCase.stepOutNumber)) }
when (testCase.caseInsensitive) {
"Both" -> builder.withMatchCaseInsensitive(true)
"Fields" -> builder.withMatchFieldNamesCaseInsensitive(true)
"None" -> Unit
else -> throw IllegalArgumentException("Unexpected value for caseInsensitive: ${testCase.caseInsensitive}")
}
val extractor = builder.buildExtractor()
val out = ION.newEmptyList()
extractor.match(ION.newReader(testCase.data), out)
assertEquals(testCase.expected, out, testCase.toString())
}
@ParameterizedTest
@MethodSource("testCases")
open fun testSearchPathsMatchCurrentValue(testCase: TestCase) {
if (testCase.hasMultipleTopLevelValues) {
// For simplicity, skip tests with multiple top-level values. This will be tested via other test methods.
return
}
val builder = PathExtractorBuilder.standard()
testCase.searchPaths.forEach { builder.withSearchPath(it, collectToIonList(testCase.stepOutNumber)) }
val extractor = builder.buildExtractor()
val out = ION.newEmptyList()
val reader = ION.newReader(testCase.data)
reader.next()
val depth = reader.depth
extractor.matchCurrentValue(reader, out)
assertEquals(depth, reader.depth)
assertEquals(testCase.expected, out, testCase.toString())
}
@ParameterizedTest
@EnumSource(API::class)
fun testCorrectCallbackCalled(api: API) {
var timesCallback1Called = 0
var timesCallback2Called = 0
val extractor: PathExtractor = PathExtractorBuilder.standard()
.withSearchPath("(foo)") { _ ->
timesCallback1Called++
0
}
.withSearchPath("(bar)") { _ ->
timesCallback2Called++
0
}
.buildExtractor()
api.match(extractor, ION.newReader("{ bar: 1, bar: 2, foo: 3 }"))
assertAll(
{ assertEquals(1, timesCallback1Called) },
{ assertEquals(2, timesCallback2Called) }
)
}
@Test
fun matchCurrentValueOnlyMatchesCurrentValue() {
val extractor1 = PathExtractorBuilder.standard()
.withSearchPath("(foo)", collectToIonList(0))
.buildExtractor()
val extractor2 = PathExtractorBuilder.standard()
.withSearchPath("(*)", collectToIonList(1))
.withMatchRelativePaths(true)
.buildExtractor()
val reader = ION.newReader("{foo: 123, foo: [456]} {bar: [42, 43, 44]} end")
val out = ION.newEmptyList()
assertEquals(IonType.STRUCT, reader.next())
extractor1.matchCurrentValue(reader, out)
assertEquals(ION.singleValue("[123, [456]]"), out)
assertEquals(IonType.STRUCT, reader.next())
reader.stepIn()
assertEquals(IonType.LIST, reader.next())
assertEquals("bar", reader.fieldName)
extractor2.matchCurrentValue(reader, out)
assertEquals(ION.singleValue("[123, [456], 42]"), out)
assertEquals(1, reader.depth)
reader.stepOut()
assertEquals(IonType.SYMBOL, reader.next())
assertEquals("end", reader.stringValue())
assertNull(reader.next())
}
@Test
fun matchCurrentValueWhenNotPositionedOnValueFails() {
val extractor = PathExtractorBuilder.standard()
.withSearchPath("(foo)") { _ -> 0 }
.buildExtractor()
val reader = ION.newReader("[{foo: 1}]")
val exception = assertThrows { extractor.matchCurrentValue(reader) }
assertEquals("reader must be positioned at a value; call IonReader.next() first.", exception.message)
}
@ParameterizedTest
@EnumSource(API::class)
fun readerAtInvalidDepth(api: API) {
val extractor = PathExtractorBuilder.standard()
.withSearchPath("(foo)") { _ -> 0 }
.buildExtractor()
val reader = ION.newReader("[{foo: 1}]")
assertTrue(reader.next() != null)
reader.stepIn()
val exception = assertThrows { api.match(extractor, reader) }
assertEquals("reader must be at depth zero, it was at: 1", exception.message)
}
@ParameterizedTest
@EnumSource(API::class)
fun matchRelative(api: API) {
val extractor = PathExtractorBuilder.standard()
.withMatchRelativePaths(true)
.withSearchPath("(foo)", collectToIonList(0))
.buildExtractor()
val reader = ION.newReader("[{foo: 1}]")
assertTrue(reader.next() != null)
reader.stepIn()
val out = ION.newEmptyList()
api.match(extractor, reader, out)
assertEquals(ION.singleValue("[1]"), out)
}
@ParameterizedTest
@EnumSource(API::class)
fun stepOutMoreThanPermitted(api: API) {
val extractor = PathExtractorBuilder.standard()
.withSearchPath("(foo)") { _ -> 200 }
.buildExtractor()
val exception = assertThrows {
api.match(extractor, ION.newReader("{foo: 1}"))
}
assertEquals("Callback return cannot be greater than the reader current relative depth. " +
"return: 200, relative reader depth: 1", exception.message)
}
@ParameterizedTest
@EnumSource(API::class)
fun stepOutMoreThanPermittedWithRelative(api: API) {
val extractor = PathExtractorBuilder.standard()
.withMatchRelativePaths(true)
// even though you could step out twice in reader you can't given the initial reader depth
.withSearchPath("(bar)") { _ -> 2 }
.buildExtractor()
val newReader = ION.newReader("{foo: {bar: 1}}")
newReader.next()
newReader.stepIn() // positioned at the beginning of {bar: 1}
val exception = assertThrows {
api.match(extractor, newReader)
}
assertEquals("Callback return cannot be greater than the reader current relative depth. return: 2, " +
"relative reader depth: 1", exception.message)
}
@ParameterizedTest
@EnumSource(API::class)
fun nestedSearchPaths(api: API) {
// Test only that the correct callbacks were called as reading the value for (foo)
// will advance the reader making (foo bar) not match
val counter = mutableMapOf(
"()" to 0,
"(foo)" to 0,
"(foo bar)" to 0
)
val extractor = PathExtractorBuilder.standard().apply {
counter.forEach { (sp, _) ->
withSearchPath(sp) { _ ->
counter[sp] = counter[sp]!! + 1
0
}
}
}.buildExtractor()
api.match(extractor, ION.newReader("{foo: {bar: 1}}"))
assertEquals(3, counter.size)
assertEquals(1, counter["()"])
assertEquals(1, counter["(foo)"])
assertEquals(1, counter["(foo bar)"])
}
// Invalid configuration -----------------------------------------------------------------------------
@Test
fun nullStringPath() {
val exception = assertThrows {
PathExtractorBuilder.standard().withSearchPath(null as String?, emptyCallback)
}
assertEquals("searchPathAsIon cannot be null", exception.message)
}
@Test
fun nullListPath() {
val exception = assertThrows {
PathExtractorBuilder.standard().withSearchPath(null as List?, emptyCallback, emptyArray())
}
assertEquals("pathComponents cannot be null", exception.message)
}
@Test
fun nullCallback() {
val exception = assertThrows {
val callback: java.util.function.Function? = null
PathExtractorBuilder.standard().withSearchPath("(foo)", callback)
}
assertEquals("callback cannot be null", exception.message)
}
@Test
fun emptySearchPath() {
val exception = assertThrows {
PathExtractorBuilder.standard().withSearchPath("", emptyCallback)
}
assertEquals("ionPathExpression cannot be empty", exception.message)
}
@Test
fun searchPathNotSequence() {
val exception = assertThrows {
PathExtractorBuilder.standard().withSearchPath("1", emptyCallback)
}
assertEquals("ionPathExpression must be a s-expression or list", exception.message)
}
private fun newReader(value: IonValue, isBinary: Boolean): IonReader {
val baos = ByteArrayOutputStream()
val ionWriter = if (isBinary) IonBinaryWriterBuilder.standard().build(baos) else IonTextWriterBuilder.standard().build(baos)
value.writeTo(ionWriter)
ionWriter.close()
return IonReaderBuilder.standard().build(baos.toByteArray())
}
@ParameterizedTest
@ValueSource(strings = ["binary", "text"])
fun evaluateSecondPathOnTheSameValueAfterTheFirstPathMatches(encoding: String) {
val value = ION.singleValue("{col1:\"foo\", col2:[{col21:\"bar\",col22:12}]}") as IonStruct
val ionReader = newReader(value, encoding == "binary")
val extractor = PathExtractorBuilder.standard().withSearchPath("(col2)") { ionReader1: IonReader? ->
val actualData = ION.newValue(ionReader1)
assertEquals(value["col2"], actualData)
0
}.withSearchPath("(col1)") { _ -> 0 }.buildExtractor()
extractor.match(ionReader)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy