Skip to content

Commit

Permalink
Refactors ranges and adds writers for ValidValues and TimestampPrecision
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt committed Oct 3, 2023
1 parent 7f1ce92 commit 434b8b0
Show file tree
Hide file tree
Showing 18 changed files with 295 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,18 @@ class ConsistentDecimal(val bigDecimalValue: BigDecimal) : Comparable<Consistent
*/
@JvmStatic
fun fromIonNumber(ionNumber: IonNumber) = ConsistentDecimal(ionNumber.bigDecimalValue())

/**
* Translates a long value into a [ConsistentDecimal] with a scale of zero.
*/
@JvmStatic
fun valueOf(long: Long) = ConsistentDecimal(BigDecimal.valueOf(long))

Check warning on line 34 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ConsistentDecimal.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ConsistentDecimal.kt#L34

Added line #L34 was not covered by tests

/**
* Translates a [Double] into a [ConsistentDecimal], using the [Double]'s canonical string representation
* provided by the [Double.toString] method.
*/
@JvmStatic
fun valueOf(double: Double) = ConsistentDecimal(BigDecimal.valueOf(double))

Check warning on line 41 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ConsistentDecimal.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ConsistentDecimal.kt#L41

Added line #L41 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,14 @@ class ConsistentTimestamp(val timestampValue: Timestamp) : Comparable<Consistent
*/
@JvmStatic
fun fromIonTimestamp(ionTimestamp: IonTimestamp) = ConsistentTimestamp(ionTimestamp.timestampValue())

/**
* Returns a new [ConsistentTimestamp] that represents the point in time, precision and local offset defined in
* Ion format by the [CharSequence].
*
* @see Timestamp.valueOf
*/
@JvmStatic
fun valueOf(ionFormattedTimestamp: CharSequence) = ConsistentTimestamp(Timestamp.valueOf(ionFormattedTimestamp))

Check warning on line 38 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ConsistentTimestamp.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ConsistentTimestamp.kt#L38

Added line #L38 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,7 @@ interface Constraint {
*
* @see ValidValue
*/
data class ValidValues(val values: Set<ValidValue>) : Constraint
data class ValidValues(val values: Set<ValidValue>) : Constraint {
constructor(vararg values: ValidValue) : this(values.toSet())

Check warning on line 258 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/Constraint.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/Constraint.kt#L258

Added line #L258 was not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package com.amazon.ionschema.model

import com.amazon.ionschema.model.ContinuousRange.Limit

interface IContinuousRange<T> where T : Comparable<T>, T : Any {
val start: Limit<T>
val end: Limit<T>
operator fun contains(value: T): Boolean
fun intersect(that: ContinuousRange<T>): Pair<Limit<T>, Limit<T>>?
}

/**
* A range over a type that is an uncountably infinite set.
*
Expand All @@ -11,10 +20,10 @@ package com.amazon.ionschema.model
* A `ContinuousRange` is allowed to be a _degenerate interval_ (i.e. `start == end` when both limits are closed),
* but it may not be an empty interval (i.e. `start == end` when either limit is open, or `start > end`).
*/
data class ContinuousRange<T : Comparable<T>>(val start: Limit<T>, val end: Limit<T>) {
open class ContinuousRange<T : Comparable<T>> internal constructor(final override val start: Limit<T>, final override val end: Limit<T>) : IContinuousRange<T> {

private constructor(value: Limit.Closed<T>) : this(value, value)
constructor(value: T) : this(Limit.Closed(value))
internal constructor(value: T) : this(Limit.Closed(value))

Check warning on line 26 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ContinuousRange.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ContinuousRange.kt#L26

Added line #L26 was not covered by tests

sealed class Limit<out T> {
abstract val value: T?
Expand All @@ -39,7 +48,7 @@ data class ContinuousRange<T : Comparable<T>>(val start: Limit<T>, val end: Limi
* Returns the intersection of `this` `DiscreteRange` with [other].
* If the two ranges do not intersect, returns `null`.
*/
fun intersect(that: ContinuousRange<T>): ContinuousRange<T>? {
final override fun intersect(that: ContinuousRange<T>): Pair<Limit<T>, Limit<T>>? {
val newStart = when {
this.start is Limit.Unbounded -> that.start
that.start is Limit.Unbounded -> this.start
Expand All @@ -58,13 +67,13 @@ data class ContinuousRange<T : Comparable<T>>(val start: Limit<T>, val end: Limi
that.end is Limit.Open -> that.end
else -> this.end // They are both closed and equal
}
return if (isEmpty(newStart, newEnd)) null else ContinuousRange(newStart, newEnd)
return if (isEmpty(newStart, newEnd)) null else newStart to newEnd
}

/**
* Checks whether the given value is contained within this range.
*/
operator fun contains(value: T): Boolean = start.isBelow(value) && end.isAbove(value)
final override operator fun contains(value: T): Boolean = start.isBelow(value) && end.isAbove(value)

private fun Limit<T>.isAbove(other: T) = when (this) {
is Limit.Closed -> value >= other
Expand All @@ -88,11 +97,23 @@ data class ContinuousRange<T : Comparable<T>>(val start: Limit<T>, val end: Limi
return if (exclusive) start.value!! >= end.value!! else start.value!! > end.value!!
}

override fun toString(): String {
final override fun toString(): String {
val lowerBrace = if (start is Limit.Closed) '[' else '('
val lowerValue = start.value ?: " "
val upperValue = end.value ?: " "
val upperBrace = if (end is Limit.Closed) ']' else ')'
return "$lowerBrace$lowerValue,$upperValue$upperBrace"
}

final override fun equals(other: Any?): Boolean {
return other is ContinuousRange<*> &&
other.start == start &&
other.end == end

Check warning on line 111 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ContinuousRange.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ContinuousRange.kt#L110-L111

Added lines #L110 - L111 were not covered by tests
}

override fun hashCode(): Int {
var result = start.hashCode()
result = 31 * result + end.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class DiscreteIntRange private constructor(private val delegate: ContinuousRange
* ```
*/
fun negate() = DiscreteIntRange(delegate.end.value?.let { it * -1 }, delegate.start.value?.let { it * -1 })
fun intersect(other: DiscreteIntRange): DiscreteIntRange? = delegate.intersect(other.delegate)?.let { DiscreteIntRange(it) }
fun intersect(other: DiscreteIntRange): DiscreteIntRange? = delegate.intersect(other.delegate)?.let { (a, b) -> DiscreteIntRange(ContinuousRange(a, b)) }

operator fun contains(value: Int): Boolean = delegate.contains(value)
override fun toString() = delegate.toString()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package com.amazon.ionschema.model

/**
* A [ContinuousRange] of [TimestampPrecisionValue].
* `TimestampPrecision` is a discrete measurement (i.e. there is no fractional number of digits of precision).
* However, because Ion Schema models timestamp precision as an enum, there are possible precisions that exist between
* the available enum values. For example, `timestamp_precision: range::[exclusive::second, exclusive::millisecond]`
* allows 1 or 2 digits of precision for the fractional seconds of a timestamp.
*/
class TimestampPrecisionRange(start: Limit<TimestampPrecisionValue>, end: Limit<TimestampPrecisionValue>) : ContinuousRange<TimestampPrecisionValue>(start, end) {
private constructor(value: Limit.Closed<TimestampPrecisionValue>) : this(value, value)
constructor(value: TimestampPrecisionValue) : this(Limit.Closed(value))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,35 @@ import com.amazon.ion.IonValue

/**
* Represents an argument to the `valid_values` constraint.
*
* Consumers of `ion-schema-kotlin` MAY NOT implement this interface.
*
* @see [Constraint.ValidValues]
*/
sealed class ValidValue {
// TODO: Make "sealed" when updating to kotlin 1.5 or higher.
interface ValidValue {
/**
* A single Ion value. May not be annotated.
* Ignoring annotations, this value is compared using Ion equivalence with data that is being validated.
* @see [Constraint.ValidValues]
*/
data class Value(val value: IonValue) : ValidValue() {
data class Value(val value: IonValue) : ValidValue {
init { require(value.typeAnnotations.isEmpty()) { "valid value may not be annotated" } }
}

/**
* A range of numbers.
* A range over Ion numbers.
*/
data class IonNumberRange(val range: NumberRange) : ValidValue()
class NumberRange(start: Limit<ConsistentDecimal>, end: Limit<ConsistentDecimal>) : ValidValue, ContinuousRange<ConsistentDecimal>(start, end) {
private constructor(value: Limit.Closed<ConsistentDecimal>) : this(value, value)
constructor(value: ConsistentDecimal) : this(Limit.Closed(value))

Check warning on line 28 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ValidValue.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ValidValue.kt#L27-L28

Added lines #L27 - L28 were not covered by tests
}

/**
* A range of timestamp values.
*/
data class IonTimestampRange(val range: TimestampRange) : ValidValue()
class TimestampRange(start: Limit<ConsistentTimestamp>, end: Limit<ConsistentTimestamp>) : ValidValue, ContinuousRange<ConsistentTimestamp>(start, end) {
private constructor(value: Limit.Closed<ConsistentTimestamp>) : this(value, value)
constructor(value: ConsistentTimestamp) : this(Limit.Closed(value))

Check warning on line 36 in ion-schema/src/main/kotlin/com/amazon/ionschema/model/ValidValue.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/model/ValidValue.kt#L35-L36

Added lines #L35 - L36 were not covered by tests
}
}
21 changes: 0 additions & 21 deletions ion-schema/src/main/kotlin/com/amazon/ionschema/model/aliases.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.amazon.ionschema.model

import com.amazon.ion.IonNumber
import com.amazon.ion.IonValue
import com.amazon.ion.Timestamp
import com.amazon.ionschema.util.Bag

/**
Expand All @@ -19,22 +17,3 @@ typealias OpenContentFields = Bag<Pair<String, IonValue>>
*/
@ExperimentalIonSchemaModel
typealias TypeArguments = Set<TypeArgument>

/**
* A [ContinuousRange] of [Timestamp], represented as a [ConsistentTimestamp].
*/
typealias TimestampRange = ContinuousRange<ConsistentTimestamp>

/**
* A [ContinuousRange] of [IonNumber], represented as [ConsistentDecimal]
*/
typealias NumberRange = ContinuousRange<ConsistentDecimal>

/**
* A [ContinuousRange] of [TimestampPrecisionValue].
* `TimestampPrecision` is a discrete measurement (i.e. there is no fractional number of digits of precision).
* However, because Ion Schema models timestamp precision as an enum, there are possible precisions that exist between
* the available enum values. For example, `timestamp_precision: range::[exclusive::second, exclusive::millisecond]`
* allows 1 or 2 digits of precision for the fractional seconds of a timestamp.
*/
typealias TimestampPrecisionRange = ContinuousRange<TimestampPrecisionValue>
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ internal class ValidValuesReader : ConstraintReader {
val theValidValues = theList.mapToSet {
if (it.hasTypeAnnotation("range") && it is IonList) {
when {
it.any { x -> x is IonTimestamp } -> ValidValue.IonTimestampRange(it.toTimestampRange())
it.any { x -> x is IonNumber } -> ValidValue.IonNumberRange(it.toNumberRange())
it.any { x -> x is IonTimestamp } -> it.toTimestampRange()
it.any { x -> x is IonNumber } -> it.toNumberRange()
else -> throw InvalidSchemaException("Not a valid range: $it")
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,50 @@ import com.amazon.ionschema.internal.util.islRequireNoIllegalAnnotations
import com.amazon.ionschema.model.ConsistentDecimal
import com.amazon.ionschema.model.ConsistentTimestamp
import com.amazon.ionschema.model.ContinuousRange
import com.amazon.ionschema.model.ContinuousRange.Limit
import com.amazon.ionschema.model.DiscreteIntRange
import com.amazon.ionschema.model.NumberRange
import com.amazon.ionschema.model.TimestampPrecisionRange
import com.amazon.ionschema.model.TimestampPrecisionValue
import com.amazon.ionschema.model.TimestampRange
import com.amazon.ionschema.model.ValidValue.NumberRange
import com.amazon.ionschema.model.ValidValue.TimestampRange

/**
* Converts an [IonValue] to a [TimestampRange].
*/
internal fun IonValue.toTimestampRange(): TimestampRange = toContinuousRange(ConsistentTimestamp::fromIonTimestamp)
internal fun IonValue.toTimestampRange(): TimestampRange = readRangeBoundaries(ConsistentTimestamp::fromIonTimestamp)
.let { (start, end) -> TimestampRange(start, end) }

/**
* Converts an [IonValue] to a [NumberRange].
*/
internal fun IonValue.toNumberRange(): NumberRange = toContinuousRange { it: IonNumber ->
internal fun IonValue.toNumberRange(): NumberRange = readRangeBoundaries { it: IonNumber ->
islRequire(it.isNumericValue) { "Invalid number range; range bounds must be real numbers: $this" }
ConsistentDecimal.fromIonNumber(it)
}.let {
(start, end) ->
NumberRange(start, end)
}

/**
* Converts an [IonValue] to a [TimestampPrecisionRange]
*/
internal fun IonValue.toTimestampPrecisionRange(): TimestampPrecisionRange {
return when (this) {
is IonList -> toContinuousRange { sym: IonSymbol ->
is IonList -> readRangeBoundaries { sym: IonSymbol ->
TimestampPrecisionValue.fromSymbolTextOrNull(sym.stringValue())
?: throw InvalidSchemaException("Invalid timestamp precision range; range bounds must be ${TimestampPrecisionValue.valueSymbolTexts().joinToString() }, min, or max: $this")
}
?: throw InvalidSchemaException(
"Invalid timestamp precision range; range bounds must be ${
TimestampPrecisionValue.valueSymbolTexts().joinToString()
}, min, or max: $this"

Check warning on line 51 in ion-schema/src/main/kotlin/com/amazon/ionschema/reader/internal/ranges.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/reader/internal/ranges.kt#L48-L51

Added lines #L48 - L51 were not covered by tests
)
}.let { (start, end) -> TimestampPrecisionRange(start, end) }

is IonSymbol -> {
islRequireIonNotNull(this) { "Timestamp precision value cannot be null; was: $this" }
islRequireNoIllegalAnnotations(this) { "Timestamp precision value may not have annotations" }
val precision = TimestampPrecisionValue.fromSymbolTextOrNull(stringValue())
?: throw InvalidSchemaException("Invalid timestamp precision range; range bounds must be ${TimestampPrecisionValue.valueSymbolTexts().joinToString() }, min, or max: $this")
ContinuousRange(precision)
TimestampPrecisionRange(precision)
}
else -> throw InvalidSchemaException("Invalid range; not an ion list: $this")
}
Expand All @@ -56,14 +66,14 @@ internal fun IonValue.toTimestampPrecisionRange(): TimestampPrecisionRange {
/**
* Converts an [IonValue] to a [ContinuousRange] using the given [valueFn].
*/
private inline fun <T : Comparable<T>, reified IV : IonValue> IonValue.toContinuousRange(valueFn: (IV) -> T): ContinuousRange<T> {
private inline fun <T : Comparable<T>, reified IV : IonValue> IonValue.readRangeBoundaries(valueFn: (IV) -> T): Pair<Limit<T>, Limit<T>> {
return when (this) {
is IonList -> {
islRequire(size == 2) { "Invalid range; size of list must be 2: $this" }
islRequireExactAnnotations(this, "range") { "Invalid range; missing 'range' annotation: $this" }
val lower = readContinuousRangeBoundary(BoundaryPosition.Lower, valueFn)
val upper = readContinuousRangeBoundary(BoundaryPosition.Upper, valueFn)
ContinuousRange(lower, upper)
return lower to upper
}
else -> throw InvalidSchemaException("Invalid range; not an ion list: $this")
}
Expand Down Expand Up @@ -116,18 +126,18 @@ private fun IonList.readDiscreteIntRangeBoundary(bp: BoundaryPosition): Int? {
/**
* Reads and validates a single endpoint of a continuous value range.
*/
private inline fun <T : Comparable<T>, reified IV : IonValue> IonList.readContinuousRangeBoundary(boundaryPosition: BoundaryPosition, valueFn: (IV) -> T): ContinuousRange.Limit<T> {
private inline fun <T : Comparable<T>, reified IV : IonValue> IonList.readContinuousRangeBoundary(boundaryPosition: BoundaryPosition, valueFn: (IV) -> T): Limit<T> {
val b = get(boundaryPosition.idx) ?: throw InvalidSchemaException("Invalid range; missing $boundaryPosition boundary value: $this")
return if (b is IonSymbol && b.stringValue() == boundaryPosition.symbol) {
islRequire(b.typeAnnotations.isEmpty()) { "Invalid range; min/max may not be annotated: $this" }
ContinuousRange.Limit.Unbounded
Limit.Unbounded
} else {
val value = islRequireIonTypeNotNull<IV>(b) { "Invalid range; $boundaryPosition boundary of range must be '${boundaryPosition.symbol}' or a non-null ${IV::class.simpleName}" }.let(valueFn)
val exclusive = readBoundaryExclusivity(boundaryPosition)
if (exclusive) {
ContinuousRange.Limit.Open(value)
Limit.Open(value)
} else {
ContinuousRange.Limit.Closed(value)
Limit.Closed(value)
}
}
}
Expand Down
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.writeTimestampPrecisionRange

@ExperimentalIonSchemaModel
internal object TimestampPrecisionWriter : ConstraintWriter {
override val supportedClasses = setOf(Constraint.TimestampPrecision::class)

Check warning on line 13 in ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/TimestampPrecisionWriter.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/TimestampPrecisionWriter.kt#L13

Added line #L13 was not covered by tests

override fun IonWriter.write(c: Constraint) {
check(c is Constraint.TimestampPrecision)
setFieldName("timestamp_precision")
writeTimestampPrecisionRange(c.range)

Check warning on line 18 in ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/TimestampPrecisionWriter.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/TimestampPrecisionWriter.kt#L17-L18

Added lines #L17 - L18 were not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.model.ValidValue
import com.amazon.ionschema.writer.internal.writeIonValue
import com.amazon.ionschema.writer.internal.writeNumberRange
import com.amazon.ionschema.writer.internal.writeTimestampRange
import com.amazon.ionschema.writer.internal.writeToList

@ExperimentalIonSchemaModel
internal object ValidValuesWriter : ConstraintWriter {
override val supportedClasses = setOf(Constraint.ValidValues::class)

Check warning on line 17 in ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ValidValuesWriter.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ValidValuesWriter.kt#L17

Added line #L17 was not covered by tests

override fun IonWriter.write(c: Constraint) {
check(c is Constraint.ValidValues)
setFieldName("valid_values")
writeToList(c.values) {
when (it) {

Check warning on line 23 in ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ValidValuesWriter.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ValidValuesWriter.kt#L21-L23

Added lines #L21 - L23 were not covered by tests
is ValidValue.NumberRange -> writeNumberRange(it)
is ValidValue.TimestampRange -> writeTimestampRange(it)
is ValidValue.Value -> writeIonValue(it.value)
}
}

Check warning on line 28 in ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ValidValuesWriter.kt

View check run for this annotation

Codecov / codecov/patch

ion-schema/src/main/kotlin/com/amazon/ionschema/writer/internal/constraints/ValidValuesWriter.kt#L27-L28

Added lines #L27 - L28 were not covered by tests
}
}
Loading

0 comments on commit 434b8b0

Please sign in to comment.