Skip to content

Commit

Permalink
Variadic record elements (#67)
Browse files Browse the repository at this point in the history
* Support variadic record elements (#44)
  • Loading branch information
dlurton authored Jan 28, 2021
1 parent 7972a36 commit 7e9ae0f
Show file tree
Hide file tree
Showing 13 changed files with 4,459 additions and 810 deletions.
3 changes: 1 addition & 2 deletions pig-runtime/src/org/partiql/pig/runtime/ErrorHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
package org.partiql.pig.runtime

import com.amazon.ionelement.api.AnyElement
import com.amazon.ionelement.api.SexpElement
import com.amazon.ionelement.api.IonElement
import com.amazon.ionelement.api.IonLocation
import com.amazon.ionelement.api.SexpElement
import com.amazon.ionelement.api.head
import com.amazon.ionelement.api.location

Expand Down
44 changes: 36 additions & 8 deletions pig-runtime/src/org/partiql/pig/runtime/IntermediateRecord.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@ import com.amazon.ionelement.api.location
import com.amazon.ionelement.api.tail

/**
* Contains an "intermediate" representation of a Pig record.
* Contains an "intermediate" representation of a PIG record.
*
* This is a helper function that helps reduce the complexity of the template and the size of
* the generated code.
* This class was intended only for use by PIG-generated domain serialization code. Human written client code
* should avoid using this class.
*
* Single use only. Discard after extracting all field values with [processRequiredField] or [processOptionalField].
* Instances are single use only and should be discarded after extracting all field values with [processRequiredField]
* or [processOptionalField].
*/
class IntermediateRecord(
/** The tag of the record. Used for error reporting purposes only. */
private val recordTagName: String,
/** The location of the record within the data being transformer. */
private val location: IonLocation?,
/** The fields and their values. */
fields: Map<String, AnyElement>
fields: Map<String, List<AnyElement>>
) {
private val fieldMap = fields.toMutableMap()

Expand All @@ -45,7 +46,10 @@ class IntermediateRecord(
* [deserFunc] to perform deserialization. Returns `null` if the field does not exist in [fieldMap].
*/
fun <T> processOptionalField(fieldName: String, deserFunc: (AnyElement) -> T): T? =
fieldMap.remove(fieldName)?.let { deserFunc(it) }
fieldMap.remove(fieldName)?.let { values: List<AnyElement> ->
values.requireArityOrMalformed(fieldName, 1)
deserFunc(values.single())
}

/**
* Same as [processOptionalField] but throws [MalformedDomainDataException] if the field does not exist
Expand All @@ -55,6 +59,31 @@ class IntermediateRecord(
processOptionalField(fieldName, deserFunc)
?: errMalformed(location, "Required field '${fieldName}' was not found within '$recordTagName' record")

/**
* Processes a variadic record field.
*
* Throws [MalformedDomainDataException] if [minArity] is > 0 and the field is not present.
*/
fun <T> processVariadicField(fieldName: String, minArity: Int, deserFunc: (AnyElement) -> T): List<T> {
val foundFieldValues = fieldMap.remove(fieldName) ?: emptyList()
foundFieldValues.requireArityOrMalformed(fieldName, minArity..Int.MAX_VALUE)
return foundFieldValues.map {
deserFunc(it)
}
}

private fun List<AnyElement>.requireArityOrMalformed(fieldName: String, size: Int) =
this@requireArityOrMalformed.requireArityOrMalformed(fieldName, IntRange(size, size))

private fun List<AnyElement>.requireArityOrMalformed(fieldName: String, arityRange: IntRange) {
if(this.size !in arityRange) {
errMalformed(
location,
"$arityRange values(s) were required to for field '$fieldName' of record '$recordTagName' " +
"but ${this.size} was/were supplied.")
}
}

/**
* After all required an optional fields in a record have been processed by the transformer, this
* function should be invoked to throw a [MalformedDomainDataException] if any unprocessed fields remain in
Expand Down Expand Up @@ -84,8 +113,7 @@ fun SexpElement.transformToIntermediateRecord(): IntermediateRecord {

val fieldMap = recordFields.map { field: AnyElement ->
val fieldSexp = field.asSexp()
fieldSexp.requireArityOrMalformed(1)
fieldSexp.head.symbolValue to fieldSexp.tail.head
fieldSexp.head.symbolValue to fieldSexp.values.tail
}.toMap()

return IntermediateRecord(recordTagName, this.metas.location, fieldMap)
Expand Down
90 changes: 64 additions & 26 deletions pig-runtime/test/org/partiql/pig/runtime/IntermediateRecordTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@

package org.partiql.pig.runtime

import com.amazon.ionelement.api.AnyElement
import com.amazon.ionelement.api.IonElement
import com.amazon.ionelement.api.IonElementLoaderOptions
import com.amazon.ionelement.api.IonTextLocation
import com.amazon.ionelement.api.createIonElementLoader
import com.amazon.ionelement.api.ionInt
import org.junit.jupiter.api.Assertions.*
import com.amazon.ionelement.api.loadSingleElement
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.fail
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource

class IntermediateRecordTests {
private val oneOne = IonTextLocation(1, 1)
Expand All @@ -35,11 +39,12 @@ class IntermediateRecordTests {
(some_record
(foo 1)
(bar 2)
(bat 3))
(bat 3)
(variadic_foo 4)
(variadic_empty))
""".trimIndent()

val ir = createIonElementLoader(IonElementLoaderOptions(includeLocationMeta = true))
.loadSingleElement(someRecord).asSexp().transformToIntermediateRecord()
val ir = createIntermediateRecord(someRecord)

val foundFields = mutableListOf<IonElement>()

Expand All @@ -53,33 +58,66 @@ class IntermediateRecordTests {
// Optional field that's not present
ir.processOptionalField("baz") { foundFields.add(it) }

// Variadic field with minimum arity of 1
ir.processVariadicField("variadic_foo", 1) { foundFields.add(it) }

// Variadic field with minimum arity of 0 that is present with no values
// (should be removed so that the call to malformedIfAnyUnprocessedFieldsRemain does not throw)
ir.processVariadicField("variadic_empty", 0) { foundFields.add(it) }

// Variadic field with minimum arity of 0 that is not present (should not throw)
ir.processVariadicField("variadic_not_present", 0) { fail("should not get called")}

assertDoesNotThrow { ir.malformedIfAnyUnprocessedFieldsRemain() }
assertEquals(listOf(ionInt(1), ionInt(2), ionInt(3)), foundFields)
assertEquals(listOf(ionInt(1), ionInt(2), ionInt(3), ionInt(4)), foundFields)
}

@Test
fun requiredFieldMissing() {
val ir = createIntermediateRecord(mapOf("foo" to ionInt(1).asAnyElement()))
@ParameterizedTest
@MethodSource("parametersForMalformedTest")
fun malformedTests(tc: MalformedTestCase) {
val ex = assertThrows<MalformedDomainDataException> {
ir.processRequiredField("bad_field") { error("should not be invoked") }
tc.blockThrowingMalformedDomainDataException()
}
tc.messageMustContainStrings.forEach {
assertTrue(ex.message!!.contains(it), "exception message must contain '$it'")
}
assertTrue(ex.message!!.contains("bad_field"))
assertEquals(oneOne, ex.location)
}

@Test
fun extraFields() {
val ir = createIntermediateRecord(mapOf("foo" to ionInt(1).asAnyElement()))
val ex = assertThrows<MalformedDomainDataException>{ ir.malformedIfAnyUnprocessedFieldsRemain() }

assertTrue(ex.message!!.contains("foo"))
assertEquals(oneOne, ex.location)
companion object {
@JvmStatic
@Suppress("UNUSED")
fun parametersForMalformedTest() = listOf(
MalformedTestCase(
"required field missing",
{ createIntermediateRecord("(some_record (foo 1))").processRequiredField("bad_field") { fail("should not be called")} },
listOf("bad_field")),
MalformedTestCase(
"required field arity too high",
{ createIntermediateRecord("(some_record (foo 1 2))").processRequiredField("foo") { fail("should not be called")} },
listOf("foo", "1..1")),
MalformedTestCase(
"optional field arity too high",
{ createIntermediateRecord("(some_record (foo 1 2))").processOptionalField("foo") { fail("should not be called")} },
listOf("foo", "1..1")),
MalformedTestCase(
"variadic arity too low",
{ createIntermediateRecord("(some_record (foo 1 2))").processVariadicField("foo", 3) { fail("should not be called")} },
listOf("foo", "3..2147483647")),
MalformedTestCase(
"variadic arity too low",
{ createIntermediateRecord("(some_record (foo 1 2))").malformedIfAnyUnprocessedFieldsRemain() },
listOf("Unexpected", "foo"))
)

private fun createIntermediateRecord(recordIonText: String): IntermediateRecord =
loadSingleElement(recordIonText, IonElementLoaderOptions(includeLocationMeta = true))
.asSexp()
.transformToIntermediateRecord()

data class MalformedTestCase(
val name: String,
val blockThrowingMalformedDomainDataException: () -> Unit,
val messageMustContainStrings: List<String>)
}


private fun createIntermediateRecord(fields: Map<String, AnyElement>): IntermediateRecord =
IntermediateRecord(
recordTagName = "some_tag",
location = oneOne,
fields = fields)
}
Loading

0 comments on commit 7e9ae0f

Please sign in to comment.