Skip to content

Commit

Permalink
Adds writers for range-based constraints (#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt authored Oct 3, 2023
1 parent f9042cb commit 7f1ce92
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<KClass<out Constraint>> = 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.")
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<KClass<out Constraint>>,
/**
* 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<Pair<Constraint, String>>
) {
/** Runs the test cases given in [writeTestCases]. */
@ParameterizedTest
@MethodSource("getWriteTestCases")
private fun `writer should be able to write constraint`(testCase: Pair<Constraint, String>) = runWriteCase(writer, testCase)

/** Helper function that can be used by subclasses to run additional "write" test cases */
internal fun runWriteCase(writer: ConstraintWriter, testCase: Pair<Constraint, String>) {
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<IonWriter>()
val constraint = mockk<Constraint>()
assertThrows<IllegalStateException> {
writer.writeTo(ionWriter, constraint)
}
}
}
Original file line number Diff line number Diff line change
@@ -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",
),
)
Original file line number Diff line number Diff line change
@@ -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",
)
)
Original file line number Diff line number Diff line change
@@ -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",
)
)

0 comments on commit 7f1ce92

Please sign in to comment.