From 7f1ce92399f18c5bf0c2c995f90133d52b748ff8 Mon Sep 17 00:00:00 2001 From: Matthew Pope <81593196+popematt@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:24:49 -0700 Subject: [PATCH] Adds writers for range-based constraints (#290) --- .../internal/constraints/ExponentWriter.kt | 19 +++++ .../constraints/LengthConstraintsWriter.kt | 43 +++++++++++ .../internal/constraints/PrecisionWriter.kt | 20 +++++ .../ionschema/writer/internal/ranges.kt | 22 ++++++ .../constraints/ConstraintTestBase.kt | 73 +++++++++++++++++++ .../constraints/ExponentWriterTest.kt | 20 +++++ .../LengthConstraintsWriterTest.kt | 37 ++++++++++ .../constraints/PrecisionWriterTest.kt | 20 +++++ 8 files changed, 254 insertions(+) create mode 100644 ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriter.kt create mode 100644 ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriter.kt create mode 100644 ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriter.kt create mode 100644 ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/ranges.kt create mode 100644 ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ConstraintTestBase.kt create mode 100644 ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriterTest.kt create mode 100644 ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriterTest.kt create mode 100644 ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriterTest.kt diff --git a/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriter.kt b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriter.kt new file mode 100644 index 0000000..aeb80a7 --- /dev/null +++ b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriter.kt @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ion.IonWriter +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.ExperimentalIonSchemaModel +import com.amazon.ionschema.writer.internal.writeRange + +@ExperimentalIonSchemaModel +internal object ExponentWriter : ConstraintWriter { + override val supportedClasses = setOf(Constraint.Exponent::class) + override fun IonWriter.write(c: Constraint) { + check(c is Constraint.Exponent) + setFieldName("exponent") + writeRange(c.range) + } +} diff --git a/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriter.kt b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriter.kt new file mode 100644 index 0000000..c8a4b83 --- /dev/null +++ b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriter.kt @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ion.IonWriter +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.ExperimentalIonSchemaModel +import com.amazon.ionschema.writer.internal.writeRange +import kotlin.reflect.KClass + +@ExperimentalIonSchemaModel +internal object LengthConstraintsWriter : ConstraintWriter { + + override val supportedClasses: Set> = setOf( + Constraint.ByteLength::class, + Constraint.CodepointLength::class, + Constraint.ContainerLength::class, + Constraint.Utf8ByteLength::class, + ) + + override fun IonWriter.write(c: Constraint) { + when (c) { + is Constraint.ByteLength -> { + setFieldName("byte_length") + writeRange(c.range) + } + is Constraint.CodepointLength -> { + setFieldName("codepoint_length") + writeRange(c.range) + } + is Constraint.ContainerLength -> { + setFieldName("container_length") + writeRange(c.range) + } + is Constraint.Utf8ByteLength -> { + setFieldName("utf8_byte_length") + writeRange(c.range) + } + else -> throw IllegalStateException("Unsupported constraint. Should be unreachable.") + } + } +} diff --git a/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriter.kt b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriter.kt new file mode 100644 index 0000000..196e550 --- /dev/null +++ b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriter.kt @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ion.IonWriter +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.ExperimentalIonSchemaModel +import com.amazon.ionschema.writer.internal.writeRange + +@ExperimentalIonSchemaModel +internal object PrecisionWriter : ConstraintWriter { + override val supportedClasses = setOf(Constraint.Precision::class) + + override fun IonWriter.write(c: Constraint) { + check(c is Constraint.Precision) + setFieldName("precision") + writeRange(c.range) + } +} diff --git a/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/ranges.kt b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/ranges.kt new file mode 100644 index 0000000..060d709 --- /dev/null +++ b/ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/ranges.kt @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal + +import com.amazon.ion.IonWriter +import com.amazon.ionschema.model.DiscreteIntRange + +internal fun IonWriter.writeRange(range: DiscreteIntRange) { + val (start, endInclusive) = range + if (start == endInclusive) { + writeInt(start!!.toLong()) + } else { + setTypeAnnotations("range") + writeList { + start?.let { writeInt(it.toLong()) } + ?: writeSymbol("min") + endInclusive?.let { writeInt(it.toLong()) } + ?: writeSymbol("max") + } + } +} diff --git a/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ConstraintTestBase.kt b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ConstraintTestBase.kt new file mode 100644 index 0000000..c136366 --- /dev/null +++ b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ConstraintTestBase.kt @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +@file:OptIn(ExperimentalIonSchemaModel::class) + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ion.IonWriter +import com.amazon.ionschema.assertEqualIon +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.ExperimentalIonSchemaModel +import com.amazon.ionschema.writer.internal.writeStruct +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import kotlin.reflect.KClass + +/** + * Base class for eliminating boilerplate code in the constraint writer test classes. + * + * Given a [ConstraintWriter] instance, this will check: + * 1. That [ConstraintWriter.supportedClasses] returns the correct values + * 2. That [ConstraintWriter.writeTo] throws if called for the wrong constraint type + * 3. That [ConstraintWriter.writeTo] writes the expected field name and value to a struct + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class ConstraintTestBase internal constructor( + internal val writer: ConstraintWriter, + protected val expectedConstraints: Set>, + /** + * Pairs of [Constraint] instances to the field name and value that is expected for writing that constraint. E.g.: + * ``` + * listOf( + * Constraint.Exponent(DiscreteIntRange(null, 23)) to "exponent: range::[min, 23]", + * Constraint.Exponent(DiscreteIntRange(7, null)) to "exponent: range::[7, max]", + * ) + * ``` + */ + protected val writeTestCases: List> +) { + /** Runs the test cases given in [writeTestCases]. */ + @ParameterizedTest + @MethodSource("getWriteTestCases") + private fun `writer should be able to write constraint`(testCase: Pair) = runWriteCase(writer, testCase) + + /** Helper function that can be used by subclasses to run additional "write" test cases */ + internal fun runWriteCase(writer: ConstraintWriter, testCase: Pair) { + val (constraint, expectedField) = testCase + assertEqualIon("{ $expectedField }") { + it.writeStruct { + writer.writeTo(it, constraint) + } + } + } + + @Test + private fun `supportedClasses should return the correct classes`() { + assertEquals(expectedConstraints, writer.supportedClasses) + } + + @Test + private fun `attempting to write an unsupported constraint should throw an exception`() { + val ionWriter = mockk() + val constraint = mockk() + assertThrows { + writer.writeTo(ionWriter, constraint) + } + } +} diff --git a/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriterTest.kt b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriterTest.kt new file mode 100644 index 0000000..25ebdb1 --- /dev/null +++ b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/ExponentWriterTest.kt @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.DiscreteIntRange +import com.amazon.ionschema.model.ExperimentalIonSchemaModel + +@OptIn(ExperimentalIonSchemaModel::class) +class ExponentWriterTest : ConstraintTestBase( + writer = ExponentWriter, + expectedConstraints = setOf(Constraint.Exponent::class), + writeTestCases = listOf( + Constraint.Exponent(DiscreteIntRange(2, 5)) to "exponent: range::[2, 5]", + Constraint.Exponent(DiscreteIntRange(null, 23)) to "exponent: range::[min, 23]", + Constraint.Exponent(DiscreteIntRange(7, null)) to "exponent: range::[7, max]", + Constraint.Exponent(DiscreteIntRange(3, 3)) to "exponent: 3", + ), +) diff --git a/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriterTest.kt b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriterTest.kt new file mode 100644 index 0000000..4a5c6bf --- /dev/null +++ b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/LengthConstraintsWriterTest.kt @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.DiscreteIntRange +import com.amazon.ionschema.model.ExperimentalIonSchemaModel + +@OptIn(ExperimentalIonSchemaModel::class) +class LengthConstraintsWriterTest : ConstraintTestBase( + writer = LengthConstraintsWriter, + expectedConstraints = setOf( + Constraint.ByteLength::class, + Constraint.CodepointLength::class, + Constraint.ContainerLength::class, + Constraint.Utf8ByteLength::class, + ), + writeTestCases = listOf( + Constraint.ByteLength(DiscreteIntRange(2, 5)) to "byte_length: range::[2, 5]", + Constraint.ByteLength(DiscreteIntRange(null, 23)) to "byte_length: range::[min, 23]", + Constraint.ByteLength(DiscreteIntRange(7, null)) to "byte_length: range::[7, max]", + Constraint.ByteLength(DiscreteIntRange(3, 3)) to "byte_length: 3", + Constraint.CodepointLength(DiscreteIntRange(2, 5)) to "codepoint_length: range::[2, 5]", + Constraint.CodepointLength(DiscreteIntRange(null, 23)) to "codepoint_length: range::[min, 23]", + Constraint.CodepointLength(DiscreteIntRange(7, null)) to "codepoint_length: range::[7, max]", + Constraint.CodepointLength(DiscreteIntRange(3, 3)) to "codepoint_length: 3", + Constraint.CodepointLength(DiscreteIntRange(2, 5)) to "codepoint_length: range::[2, 5]", + Constraint.ContainerLength(DiscreteIntRange(null, 23)) to "container_length: range::[min, 23]", + Constraint.ContainerLength(DiscreteIntRange(7, null)) to "container_length: range::[7, max]", + Constraint.ContainerLength(DiscreteIntRange(3, 3)) to "container_length: 3", + Constraint.Utf8ByteLength(DiscreteIntRange(2, 5)) to "utf8_byte_length: range::[2, 5]", + Constraint.Utf8ByteLength(DiscreteIntRange(null, 23)) to "utf8_byte_length: range::[min, 23]", + Constraint.Utf8ByteLength(DiscreteIntRange(7, null)) to "utf8_byte_length: range::[7, max]", + Constraint.Utf8ByteLength(DiscreteIntRange(3, 3)) to "utf8_byte_length: 3", + ) +) diff --git a/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriterTest.kt b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriterTest.kt new file mode 100644 index 0000000..f0e9bd5 --- /dev/null +++ b/ion-schema/src/test/kotlin/com/amazon/ionschema/writer/internal/constraints/PrecisionWriterTest.kt @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.ionschema.writer.internal.constraints + +import com.amazon.ionschema.model.Constraint +import com.amazon.ionschema.model.DiscreteIntRange +import com.amazon.ionschema.model.ExperimentalIonSchemaModel + +@OptIn(ExperimentalIonSchemaModel::class) +class PrecisionWriterTest : ConstraintTestBase( + writer = PrecisionWriter, + expectedConstraints = setOf(Constraint.Precision::class), + writeTestCases = listOf( + Constraint.Precision(DiscreteIntRange(2, 5)) to "precision: range::[2, 5]", + Constraint.Precision(DiscreteIntRange(null, 23)) to "precision: range::[min, 23]", + Constraint.Precision(DiscreteIntRange(7, null)) to "precision: range::[7, max]", + Constraint.Precision(DiscreteIntRange(3, 3)) to "precision: 3", + ) +)