Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds changes to read full schema document #273

Merged
merged 2 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ internal fun <T : IonValue> T.markReadOnly(): T {
return this
}

/**
* Guarantees that the returned value is read-only, creating a read-only clone if this value is not already read-only.
*/
internal fun <T : IonValue> T.getReadOnlyClone(): T = if (this.isReadOnly) this else this.clone().markReadOnly() as T

/**
* Returns the Ion Schema type name for an IonType.
* TLDR; "DATAGRAM" is "document" and every other name is simply converted to lowercase.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface IonSchemaReader {
* the given Ion. Reporting all errors is a best-effort attempt because the presence of one error can mask other
* errors.
*/
fun readSchema(document: List<IonValue>, failFast: Boolean = false): IonSchemaResult<SchemaDocument, List<ReadError>>
fun readSchema(document: Iterable<IonValue>, failFast: Boolean = false): IonSchemaResult<SchemaDocument, List<ReadError>>

/**
* Reads a [SchemaDocument], throwing an [InvalidSchemaException][com.amazon.ionschema.InvalidSchemaException]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,123 @@
package com.amazon.ionschema.reader

import com.amazon.ion.IonValue
import com.amazon.ionschema.InvalidSchemaException
import com.amazon.ionschema.IonSchemaVersion
import com.amazon.ionschema.internal.util.IonSchema_2_0
import com.amazon.ionschema.internal.util.getReadOnlyClone
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import com.amazon.ionschema.model.NamedTypeDefinition
import com.amazon.ionschema.model.SchemaDocument
import com.amazon.ionschema.model.TypeDefinition
import com.amazon.ionschema.reader.internal.FooterReader
import com.amazon.ionschema.reader.internal.HeaderReader
import com.amazon.ionschema.reader.internal.ReadError
import com.amazon.ionschema.reader.internal.ReaderContext
import com.amazon.ionschema.reader.internal.TypeReaderV2_0
import com.amazon.ionschema.reader.internal.isFooter
import com.amazon.ionschema.reader.internal.isHeader
import com.amazon.ionschema.reader.internal.isTopLevelOpenContent
import com.amazon.ionschema.reader.internal.isType
import com.amazon.ionschema.reader.internal.readCatching
import com.amazon.ionschema.util.IonSchemaResult

@OptIn(ExperimentalIonSchemaModel::class)
class IonSchemaReaderV2_0 : IonSchemaReader {
private val typeReader = TypeReaderV2_0()
private val headerReader = HeaderReader(IonSchemaVersion.v2_0)
private val footerReader = FooterReader { it in userReservedFields.footer || !IonSchema_2_0.RESERVED_WORDS_REGEX.matches(it) }

override fun readSchema(document: List<IonValue>, failFast: Boolean): IonSchemaResult<SchemaDocument, List<ReadError>> {
TODO()
private enum class ReaderState(val location: String) {
Init("before version marker"),
BeforeHeader("before schema header"),
ReadingTypes("while reading types"),
}

override fun readSchema(document: Iterable<IonValue>, failFast: Boolean): IonSchemaResult<SchemaDocument, List<ReadError>> {
val context = ReaderContext(failFast = failFast)

val documentIterator = document.iterator()

val schemaDocument = try { iterateSchema(documentIterator, context) } catch (e: InvalidSchemaException) { null }

return if (schemaDocument != null && context.readErrors.isEmpty()) {
IonSchemaResult.Ok(schemaDocument)
} else {
IonSchemaResult.Err(context.readErrors) { InvalidSchemaException("$it") }
}
}

override fun readType(ion: IonValue, failFast: Boolean): IonSchemaResult<TypeDefinition, List<ReadError>> {
val context = ReaderContext(failFast = failFast)
val typeDefinition = typeReader.readOrphanedTypeDefinition(context, ion)
return if (context.readErrors.isEmpty()) {
val typeDefinition = try { typeReader.readOrphanedTypeDefinition(context, ion) } catch (e: InvalidSchemaException) { null }
return if (typeDefinition != null && context.readErrors.isEmpty()) {
IonSchemaResult.Ok(typeDefinition)
} else {
IonSchemaResult.Err(context.readErrors)
IonSchemaResult.Err(context.readErrors) { InvalidSchemaException("$it") }
}
}

override fun readNamedType(ion: IonValue, failFast: Boolean): IonSchemaResult<NamedTypeDefinition, List<ReadError>> {
val context = ReaderContext(failFast = failFast)
val typeDefinition = typeReader.readNamedTypeDefinition(context, ion)
return if (context.readErrors.isEmpty()) {
val typeDefinition = try { typeReader.readNamedTypeDefinition(context, ion) } catch (e: InvalidSchemaException) { null }
return if (typeDefinition != null && context.readErrors.isEmpty()) {
IonSchemaResult.Ok(typeDefinition)
} else {
IonSchemaResult.Err(context.readErrors)
IonSchemaResult.Err(context.readErrors) { InvalidSchemaException("$it") }
}
}

private fun iterateSchema(documentIterator: Iterator<IonValue>, context: ReaderContext): SchemaDocument? {
val items = mutableListOf<SchemaDocument.Item>()
var state = ReaderState.Init
while (documentIterator.hasNext()) {
val value = documentIterator.next()
when {
IonSchemaVersion.isVersionMarker(value) -> {
if (state == ReaderState.Init && IonSchemaVersion.fromIonSymbolOrNull(value) == IonSchemaVersion.v2_0) {
state = ReaderState.BeforeHeader
} else {
context.reportError(ReadError(value, "unexpected version marker ${state.location}"))
}
}
isHeader(value) -> {
if (state == ReaderState.BeforeHeader) {
readCatching(context, value) { headerReader.readHeader(context, value) }?.let(items::add)
state = ReaderState.ReadingTypes
} else {
context.reportError(ReadError(value, "schema header encountered ${state.location}"))
}
}
isType(value) -> {
if (state > ReaderState.Init) {
readCatching(context, value) { typeReader.readNamedTypeDefinition(context, value) }
?.let { items.add(SchemaDocument.Item.Type(it)) }
state = ReaderState.ReadingTypes
} else {
context.reportError(ReadError(value, "type definition encountered ${state.location}"))
}
}
isFooter(value) -> {
if (state > ReaderState.Init) {
readCatching(context, value) { items.add(footerReader.readFooter(context, value)) }
break
} else {
context.reportError(ReadError(value, "schema footer encountered ${state.location}"))
}
}
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()))
} else {
context.reportError(ReadError(value, "invalid top level value ${state.location}"))
}
}
else -> {
// Drop anything before the version marker.
}
}
}
return SchemaDocument(null, IonSchemaVersion.v2_0, items.toList())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ internal class ReaderContext(
get() = _readErrors.toList()

/**
* Reports a [ReadError] to this [ReaderContext].
* If [failFast] is false, adds an error to this [ReaderContext].
* If [failFast] is true, throws the error as an [InvalidSchemaException].
* Reports a [ReadError] to this [ReaderContext], adding it to [readErrors].
* If [failFast] is `true`, also throws the error as an [InvalidSchemaException].
*/
fun reportError(error: ReadError) {
if (failFast) {
_readErrors.add(error)
InvalidSchemaException.failFast(error)
} else {
_readErrors.add(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.amazon.ion.IonText
import com.amazon.ion.IonValue
import com.amazon.ionschema.InvalidSchemaException
import com.amazon.ionschema.IonSchemaVersion
import com.amazon.ionschema.internal.util.IonSchema_2_0
import com.amazon.ionschema.internal.util.getIslOptionalField
import com.amazon.ionschema.internal.util.getIslRequiredField
import com.amazon.ionschema.internal.util.islRequire
Expand Down Expand Up @@ -150,7 +151,8 @@ internal class TypeReaderV2_0 : TypeReader {
if (readerForThisConstraint != null) {
constraints.add(readerForThisConstraint.readConstraint(context, field))
} else {
// TODO: Make sure that it's a legal field name for open content
val isLegalOpenContentFieldName = !IonSchema_2_0.RESERVED_WORDS_REGEX.matches(fieldName) || fieldName in context.userReservedFields.type
islRequire(isLegalOpenContentFieldName) { "illegal use of field name '$fieldName'" }
openContent.add(fieldName to field)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.amazon.ionschema.reader.internal

import com.amazon.ion.IonStruct
import com.amazon.ion.IonValue
import com.amazon.ionschema.InvalidSchemaException
import com.amazon.ionschema.IonSchemaVersion
import com.amazon.ionschema.internal.util.IonSchema_2_0
import com.amazon.ionschema.model.ExperimentalIonSchemaModel
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

/**
* Helper function to catch exceptions and convert them into [ReadError] or rethrow if they are a fast-fail exception.
Expand Down Expand Up @@ -35,3 +40,35 @@ internal inline fun <T : Any> Iterable<IonValue>.readAllCatching(context: Reader
internal fun invalidConstraint(value: IonValue, reason: String, constraintName: String = value.fieldName): String {
return "Illegal argument for '$constraintName' constraint; $reason: $value"
}

@OptIn(ExperimentalContracts::class)
internal fun isHeader(value: IonValue): Boolean {
contract { returns(true) implies (value is IonStruct) }
return value is IonStruct && !value.isNullValue && arrayOf("schema_header").contentDeepEquals(value.typeAnnotations)
}

@OptIn(ExperimentalContracts::class)
internal fun isFooter(value: IonValue): Boolean {
contract { returns(true) implies (value is IonStruct) }
return value is IonStruct && !value.isNullValue && arrayOf("schema_footer").contentDeepEquals(value.typeAnnotations)
}

@OptIn(ExperimentalContracts::class)
internal fun isType(value: IonValue): Boolean {
contract { returns(true) implies (value is IonStruct) }
return value is IonStruct && !value.isNullValue && arrayOf("type").contentDeepEquals(value.typeAnnotations)
}

/**
* Checks whether a given value is allowed as top-level open content.
* See https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#open-content
*/
internal fun isTopLevelOpenContent(value: IonValue): Boolean {
if (IonSchemaVersion.isVersionMarker(value)) {
return false
}
if (value.typeAnnotations.any { IonSchema_2_0.RESERVED_WORDS_REGEX.matches(it) }) {
return false
}
return true
}
4 changes: 4 additions & 0 deletions ion-schema/src/main/kotlin/com/amazon/ionschema/util/Bag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ class Bag<out E>(elements: List<E>) : Collection<E> by elements {
}

override fun hashCode(): Int = this.sumBy { it.hashCode() }

override fun toString(): String {
return "Bag[${this.joinToString()}]"
}
}
Loading