Skip to content

Commit

Permalink
Usage improvements for ISL model (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt authored Jun 2, 2023
1 parent 6157830 commit 70c75dc
Show file tree
Hide file tree
Showing 29 changed files with 153 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface Constraint {
* See relevant section in [ISL 1.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#all_of) and
* [ISL 2.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#all_of).
*/
data class AllOf(val types: TypeArgumentList) : Constraint
data class AllOf(val types: TypeArguments) : Constraint

/**
* Represents the `annotations` constraint for Ion Schema 1.0.
Expand All @@ -43,11 +43,11 @@ interface Constraint {
* [simple syntax](https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#simple-syntax).
*/
@JvmStatic
fun create(modifier: Modifier, annotationSymbols: List<IonSymbol>): AnnotationsV2 {
fun create(modifier: Modifier, annotationSymbols: Set<IonSymbol>): AnnotationsV2 {
val annotationsConstraints = mutableSetOf<Constraint>()
// If closed, constrain using `valid_values`
if (modifier == Modifier.Closed || modifier == Modifier.ClosedAndRequired) {
val validValues = annotationSymbols.map { ValidValue.Value(it) }
val validValues = annotationSymbols.mapToSet { ValidValue.Value(it) }
annotationsConstraints.add(
Element(TypeArgument.InlineType(TypeDefinition(setOf(ValidValues(validValues)))))
)
Expand All @@ -66,7 +66,7 @@ interface Constraint {
* See relevant section in [ISL 1.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#any_of) and
* [ISL 2.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#any_of).
*/
data class AnyOf(val types: TypeArgumentList) : Constraint
data class AnyOf(val types: TypeArguments) : Constraint

/**
* Represents the `byte_length` constraint.
Expand Down Expand Up @@ -106,7 +106,7 @@ interface Constraint {
* See relevant section in [ISL 1.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#contains) and
* [ISL 2.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#contains).
*/
data class Contains(val values: List<IonValue>) : Constraint
data class Contains(val values: Set<IonValue>) : Constraint

/**
* Represents the `element` constraint.
Expand Down Expand Up @@ -155,7 +155,7 @@ interface Constraint {
* See relevant section in [ISL 1.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#one_of) and
* [ISL 2.0 spec](https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#one_of).
*/
data class OneOf(val types: TypeArgumentList) : Constraint
data class OneOf(val types: TypeArguments) : Constraint

/**
* Represents the `ordered_elements` constraint.
Expand Down Expand Up @@ -221,7 +221,7 @@ interface Constraint {
*
* @see TimestampOffsetValue
*/
data class TimestampOffset(val offsets: List<TimestampOffsetValue>) : Constraint
data class TimestampOffset(val offsets: Set<TimestampOffsetValue>) : Constraint

/**
* Represents the `timestamp_precision` constraint.
Expand Down Expand Up @@ -257,5 +257,5 @@ interface Constraint {
*
* @see ValidValue
*/
data class ValidValues(val values: List<ValidValue>) : Constraint
data class ValidValues(val values: Set<ValidValue>) : Constraint
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ data class ContinuousRange<T : Comparable<T>>(val start: Limit<T>, val end: Limi
private constructor(value: Limit.Closed<T>) : this(value, value)
constructor(value: T) : this(Limit.Closed(value))

sealed class Limit<T : Comparable<T>> {
sealed class Limit<out T> {
abstract val value: T?

interface Bounded<T> { val value: T }
data class Closed<T : Comparable<T>>(override val value: T) : Limit<T>(), Bounded<T>
data class Open<T : Comparable<T>>(override val value: T) : Limit<T>(), Bounded<T>
class Unbounded<T : Comparable<T>> : Limit<T>() {
object Unbounded : Limit<Nothing>() {
override val value: Nothing? get() = null

override fun equals(other: Any?) = other is Unbounded<*>
override fun equals(other: Any?) = other is Unbounded
override fun hashCode() = 0
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class DiscreteIntRange private constructor(private val delegate: ContinuousRange
// Because we never construct Open bounds, we cannot accidentally create an empty range.
constructor(start: Int?, endInclusive: Int?) : this(
ContinuousRange(
start?.let { ContinuousRange.Limit.Closed(it) } ?: ContinuousRange.Limit.Unbounded(),
endInclusive?.let { ContinuousRange.Limit.Closed(it) } ?: ContinuousRange.Limit.Unbounded()
start?.let { ContinuousRange.Limit.Closed(it) } ?: ContinuousRange.Limit.Unbounded,
endInclusive?.let { ContinuousRange.Limit.Closed(it) } ?: ContinuousRange.Limit.Unbounded
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ package com.amazon.ionschema.model
* Represents a top-level, named type definition.
*/
@ExperimentalIonSchemaModel
data class NamedTypeDefinition(val typeName: String, val typeDefinition: TypeDefinition)
data class NamedTypeDefinition(val typeName: String, val typeDefinition: TypeDefinition) : SchemaDocument.Content
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.amazon.ionschema.model
import com.amazon.ion.IonValue
import com.amazon.ionschema.IonSchemaVersion
import com.amazon.ionschema.internal.util.islRequire
import com.amazon.ionschema.util.emptyBag

/**
* Represents an Ion Schema document.
Expand All @@ -12,12 +11,12 @@ import com.amazon.ionschema.util.emptyBag
data class SchemaDocument(
val id: String?,
val ionSchemaVersion: IonSchemaVersion,
val items: List<Item>
val items: List<Content>
) {
val header: Item.Header? = items.filterIsInstance<Item.Header>().singleOrNull()
val footer: Item.Footer? = items.filterIsInstance<Item.Footer>().singleOrNull()
val header: SchemaHeader? = items.filterIsInstance<SchemaHeader>().singleOrNull()
val footer: SchemaFooter? = items.filterIsInstance<SchemaFooter>().singleOrNull()
val declaredTypes: Map<String, NamedTypeDefinition> = let {
val typeList = items.filterIsInstance<Item.Type>().map { it.value }
val typeList = items.filterIsInstance<NamedTypeDefinition>()
val typeMap = typeList.associateBy { it.typeName }
islRequire(typeMap.size == typeList.size) {
"Conflicting type names in schema"
Expand All @@ -26,19 +25,14 @@ data class SchemaDocument(
}

/**
* Represents a top-level item in a schema document.
* Represents a top-level value in a schema document.
* Implemented by [NamedTypeDefinition], [SchemaHeader], [SchemaFooter], and [OpenContent].
* This interface is not intended to be implemented by users of the library.
*/
sealed class Item {
data class Type(val value: NamedTypeDefinition) : Item()
interface Content

data class Header(
val imports: List<HeaderImport> = emptyList(),
val userReservedFields: UserReservedFields = UserReservedFields(),
val openContent: OpenContentFields = emptyBag()
) : Item()

data class Footer(val openContent: OpenContentFields = emptyBag()) : Item()

data class OpenContent(val value: IonValue) : Item()
}
/**
* Represents top-level open content in a SchemaDocument.
*/
data class OpenContent(val value: IonValue) : Content
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.amazon.ionschema.model

import com.amazon.ionschema.util.emptyBag

/**
* Represents the schema footer for all versions of Ion Schema.
*/
@ExperimentalIonSchemaModel
data class SchemaFooter(val openContent: OpenContentFields = emptyBag()) : SchemaDocument.Content
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.amazon.ionschema.model

import com.amazon.ionschema.util.emptyBag

/**
* Represents the schema header for all versions of Ion Schema.
*/
@ExperimentalIonSchemaModel
data class SchemaHeader(
val imports: Set<HeaderImport> = emptySet(),
val userReservedFields: UserReservedFields = UserReservedFields(),
val openContent: OpenContentFields = emptyBag()
) : SchemaDocument.Content
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import com.amazon.ionschema.util.Bag
typealias OpenContentFields = Bag<Pair<String, IonValue>>

/**
* Convenience alias for a list of [TypeArgument].
* Convenience alias for a set of [TypeArgument].
*/
@ExperimentalIonSchemaModel
typealias TypeArgumentList = List<TypeArgument>
typealias TypeArguments = Set<TypeArgument>

/**
* A [ContinuousRange] of [Timestamp], represented as a [ConsistentTimestamp].
Expand Down
8 changes: 8 additions & 0 deletions ion-schema/src/main/kotlin/com/amazon/ionschema/model/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ internal fun not(c: Constraint): Constraint {
internal fun inlineType(constraints: Set<Constraint>): TypeArgument.InlineType {
return TypeArgument.InlineType(TypeDefinition(constraints))
}

/**
* Returns a set containing the results of applying the given [transform] function
* to each element in the original collection.
*/
inline fun <T, R> Iterable<T>.mapToSet(transform: (T) -> R): Set<R> {
return mapTo(HashSet(), transform)
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class IonSchemaReaderV2_0 : IonSchemaReader {
}

private fun iterateSchema(documentIterator: Iterator<IonValue>, context: ReaderContext): SchemaDocument? {
val items = mutableListOf<SchemaDocument.Item>()
val items = mutableListOf<SchemaDocument.Content>()
var state = ReaderState.Init
while (documentIterator.hasNext()) {
val value = documentIterator.next()
Expand All @@ -91,7 +91,7 @@ class IonSchemaReaderV2_0 : IonSchemaReader {
isType(value) -> {
if (state > ReaderState.Init) {
readCatching(context, value) { typeReader.readNamedTypeDefinition(context, value) }
?.let { items.add(SchemaDocument.Item.Type(it)) }
?.let { items.add(it) }
state = ReaderState.ReadingTypes
} else {
context.reportError(ReadError(value, "type definition encountered ${state.location}"))
Expand All @@ -108,7 +108,7 @@ class IonSchemaReaderV2_0 : IonSchemaReader {
state > ReaderState.Init -> {
// If we've already seen the version marker, then handle open content
if (isTopLevelOpenContent(value)) {
items.add(SchemaDocument.Item.OpenContent(value.getReadOnlyClone()))
items.add(SchemaDocument.OpenContent(value.getReadOnlyClone()))
} else {
context.reportError(ReadError(value, "invalid top level value ${state.location}"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import com.amazon.ionschema.internal.util.islRequire
import com.amazon.ionschema.internal.util.islRequireExactAnnotations
import com.amazon.ionschema.internal.util.islRequireIonTypeNotNull
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.SchemaDocument
import com.amazon.ionschema.model.SchemaFooter
import com.amazon.ionschema.util.toBag

@ExperimentalIonSchemaModel
internal class FooterReader(private val isValidOpenContentField: ReaderContext.(String) -> Boolean) {

fun readFooter(context: ReaderContext, footerValue: IonValue): SchemaDocument.Item.Footer {
fun readFooter(context: ReaderContext, footerValue: IonValue): SchemaFooter {
islRequireIonTypeNotNull<IonStruct>(footerValue) { "schema_footer must be a non-null struct; was: $footerValue" }
islRequireExactAnnotations(footerValue, "schema_footer") { "schema_footer may not have extra annotations" }

Expand All @@ -21,6 +21,6 @@ internal class FooterReader(private val isValidOpenContentField: ReaderContext.(
islRequire(unexpectedFieldNames.isEmpty()) { "Found illegal field names $unexpectedFieldNames in schema footer: $footerValue" }

val openContent = footerValue.map { it.fieldName to it }.toBag()
return SchemaDocument.Item.Footer(openContent)
return SchemaFooter(openContent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.amazon.ionschema.internal.util.islRequireOnlyExpectedFieldNames
import com.amazon.ionschema.internal.util.islRequireZeroOrOneElements
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.HeaderImport
import com.amazon.ionschema.model.SchemaDocument
import com.amazon.ionschema.model.SchemaHeader
import com.amazon.ionschema.model.UserReservedFields
import com.amazon.ionschema.util.toBag

Expand All @@ -29,7 +29,7 @@ internal class HeaderReader(private val ionSchemaVersion: IonSchemaVersion) {
/**
* Reads the header
*/
fun readHeader(context: ReaderContext, headerValue: IonValue): SchemaDocument.Item.Header {
fun readHeader(context: ReaderContext, headerValue: IonValue): SchemaHeader {
islRequire(!context.foundHeader) { "Only one schema header is allowed in a schema document." }
islRequire(!context.foundAnyType) { "Schema header must appear before any types." }
context.foundHeader = true
Expand All @@ -52,7 +52,7 @@ internal class HeaderReader(private val ionSchemaVersion: IonSchemaVersion) {
.map { it.fieldName to it }
.toBag()

return SchemaDocument.Item.Header(imports, context.userReservedFields, openContent)
return SchemaHeader(imports, context.userReservedFields, openContent)
}

/**
Expand Down Expand Up @@ -86,9 +86,9 @@ internal class HeaderReader(private val ionSchemaVersion: IonSchemaVersion) {
?: emptySet()
}

private fun loadHeaderImports(context: ReaderContext, header: IonStruct): List<HeaderImport> {
private fun loadHeaderImports(context: ReaderContext, header: IonStruct): Set<HeaderImport> {
// If there's no imports field, then there's nothing to do
val imports = header.getIslOptionalField<IonList>("imports") ?: return emptyList()
val imports = header.getIslOptionalField<IonList>("imports") ?: return emptySet()

islRequireNoIllegalAnnotations(imports) { "'imports' list may not be annotated" }

Expand All @@ -109,7 +109,7 @@ internal class HeaderReader(private val ionSchemaVersion: IonSchemaVersion) {
typeField != null -> HeaderImport.Type(schemaId, typeField.stringValue())
else -> HeaderImport.Wildcard(schemaId)
}
}
}.toSet()
}

private fun validateFieldNamesInHeader(context: ReaderContext, header: IonStruct) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.amazon.ionschema.model.DiscreteIntRange
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.NamedTypeDefinition
import com.amazon.ionschema.model.TypeArgument
import com.amazon.ionschema.model.TypeArgumentList
import com.amazon.ionschema.model.TypeArguments
import com.amazon.ionschema.model.VariablyOccurringTypeArgument

@ExperimentalIonSchemaModel
Expand All @@ -30,12 +30,12 @@ internal interface TypeReader {
fun readVariablyOccurringTypeArg(context: ReaderContext, ion: IonValue, defaultOccurs: DiscreteIntRange): VariablyOccurringTypeArgument

/**
* Reads a [TypeArgumentList].
* Reads a [TypeArguments].
*/
fun readTypeArgumentList(context: ReaderContext, ion: IonValue): TypeArgumentList {
fun readTypeArgumentList(context: ReaderContext, ion: IonValue): TypeArguments {
val constraintName = ion.fieldName!!
islRequireIonTypeNotNull<IonList>(ion) { "Illegal argument for '$constraintName' constraint; must be non-null Ion list: $ion" }
islRequire(ion.typeAnnotations.isEmpty()) { "Illegal argument for '$constraintName' constraint; must not have annotations; was: $ion" }
return ion.readAllCatching(context) { readTypeArg(context, it) }
return ion.readAllCatching(context) { readTypeArg(context, it) }.toSet()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal class AnnotationsV2Reader(private val typeReader: TypeReader) : Constra
} else {
Constraint.AnnotationsV2.Modifier.Closed
}
Constraint.AnnotationsV2.create(modifier, field.filterIsInstance<IonSymbol>())
Constraint.AnnotationsV2.create(modifier, field.filterIsInstance<IonSymbol>().toSet())
}
is IonStruct, is IonSymbol -> Constraint.AnnotationsV2(typeReader.readTypeArg(context, field))
else -> throw InvalidSchemaException(invalidConstraint(field, "must be a type argument (symbol or struct) or a list of valid annotations"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.amazon.ionschema.internal.util.islRequireIonTypeNotNull
import com.amazon.ionschema.internal.util.islRequireNoIllegalAnnotations
import com.amazon.ionschema.model.Constraint
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.mapToSet
import com.amazon.ionschema.reader.internal.ReaderContext
import com.amazon.ionschema.reader.internal.invalidConstraint

Expand All @@ -19,6 +20,6 @@ internal class ContainsReader : ConstraintReader {

islRequireIonTypeNotNull<IonList>(field) { invalidConstraint(field, "must be a non-null list") }
islRequireNoIllegalAnnotations(field) { invalidConstraint(field, "must not have annotations") }
return Constraint.Contains(field.map { it.clone() })
return Constraint.Contains(field.mapToSet { it.clone() })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.amazon.ionschema.internal.util.islRequireNotEmpty
import com.amazon.ionschema.model.Constraint
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.TimestampOffsetValue
import com.amazon.ionschema.model.mapToSet
import com.amazon.ionschema.reader.internal.ReaderContext
import com.amazon.ionschema.reader.internal.invalidConstraint

Expand All @@ -28,7 +29,7 @@ internal class TimestampOffsetReader : ConstraintReader {
return Constraint.TimestampOffset(
field.islRequireElementType<IonString>("timestamp offset list")
.islRequireNotEmpty("timestamp offset list")
.map { TimestampOffsetValue.parse(it.stringValue()) }
.mapToSet { TimestampOffsetValue.parse(it.stringValue()) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.amazon.ionschema.internal.util.islRequireNoIllegalAnnotations
import com.amazon.ionschema.model.Constraint
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.ValidValue
import com.amazon.ionschema.model.mapToSet
import com.amazon.ionschema.reader.internal.ReaderContext
import com.amazon.ionschema.reader.internal.invalidConstraint
import com.amazon.ionschema.reader.internal.toNumberRange
Expand All @@ -27,7 +28,7 @@ internal class ValidValuesReader : ConstraintReader {
islRequireNoIllegalAnnotations(field, "range") { invalidConstraint(field, "must be a range or an unannotated list of values ") }
val theList = if (field.hasTypeAnnotation("range")) listOf(field) else field

val theValidValues = theList.map {
val theValidValues = theList.mapToSet {
if (it.hasTypeAnnotation("range") && it is IonList) {
when {
it.any { x -> x is IonTimestamp } -> ValidValue.IonTimestampRange(it.toTimestampRange())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private inline fun <T : Comparable<T>, reified IV : IonValue> IonList.readContin
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()
ContinuousRange.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)
Expand Down
Loading

0 comments on commit 70c75dc

Please sign in to comment.