From 286ad00a6dcb1cdddf926910fbad91dc98a2042f Mon Sep 17 00:00:00 2001 From: Matthew Pope Date: Mon, 11 Dec 2023 12:15:06 -0800 Subject: [PATCH] Adds IonRawTextWriter_1_1 --- .../amazon/ion/impl/IonRawTextWriter_1_1.kt | 365 ++++++ .../{IonWriter_1_1.kt => IonRawWriter_1_1.kt} | 2 +- .../ion/impl/bin/IonRawBinaryWriter_1_1.kt | 2 +- .../ion/impl/IonRawTextWriterTest_1_1.kt | 1022 +++++++++++++++++ .../impl/bin/IonRawBinaryWriterTest_1_1.kt | 4 +- 5 files changed, 1391 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt rename src/main/java/com/amazon/ion/impl/{IonWriter_1_1.kt => IonRawWriter_1_1.kt} (99%) create mode 100644 src/test/java/com/amazon/ion/impl/IonRawTextWriterTest_1_1.kt diff --git a/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt new file mode 100644 index 0000000000..0f02e1182e --- /dev/null +++ b/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt @@ -0,0 +1,365 @@ +package com.amazon.ion.impl + +import com.amazon.ion.* +import com.amazon.ion.impl.IonRawTextWriter_1_1.ContainerType.* +import com.amazon.ion.util.* +import java.math.BigDecimal +import java.math.BigInteger +import java.time.Instant + +/** + * A raw writer for Ion 1.1 text. This should be combined with managed writer to handle concerns such as macros and + * possible symbol interning. + * + * Notes: + * - Never writes using "long string" syntax in order to simplify the writer. + * - Uses `[: ... ]` for expression groups. + * - Does not try to resolve symbol tokens. That is the concern of the managed writer. + */ +class IonRawTextWriter_1_1 internal constructor( + private val options: _Private_IonTextWriterBuilder, + private val output: _Private_IonTextAppender, +) : IonRawWriter_1_1 { + + enum class ContainerType { + List, + SExp, + Struct, + Macro, + ExpressionGroup, + Top, + } + + private var closed = false + + private val ancestorContainersStack: ArrayList = ArrayList() + private var currentContainer: ContainerType = Top + private var currentContainerHasValues = false + + private var isPendingSeparator = false + private var isPendingLeadingWhitespace = false + + private var fieldNameTextBuffer: CharSequence? = null + private var fieldNameIdBuffer: Int = -1 + private var hasFieldName = false + + private var annotationsTextBuffer = arrayOfNulls(8) + private var annotationsIdBuffer = IntArray(8) + private var numAnnotations = 0 + + private inline fun openValue(valueWriterExpression: () -> Unit) { + if (currentContainer == Struct) { + confirm(hasFieldName) { "Values in a struct require a field name." } + } + val separatorCharacter = when (currentContainer) { + List, Struct -> "," + Macro, SExp -> " " + Top -> options.topLevelSeparator() + ExpressionGroup -> "," + } + + if (options.isPrettyPrintOn) { + if (isPendingSeparator && !IonTextUtils.isAllWhitespace(separatorCharacter)) { + // Only bother if the separator is non-whitespace. + output.appendAscii(separatorCharacter) + } + if (isPendingSeparator || isPendingLeadingWhitespace) { + output.appendAscii(options.lineSeparator()) + output.appendAscii(" ".repeat(ancestorContainersStack.size * 2)) + } + } else if (isPendingSeparator) { + output.appendAscii(separatorCharacter) + } + + isPendingSeparator = false + + if (hasFieldName) { + if (fieldNameTextBuffer != null) { + output.printSymbol(fieldNameTextBuffer) + output.appendAscii(':') + if (options.isPrettyPrintOn) output.appendAscii(" ") + fieldNameTextBuffer = null + } else { + output.appendAscii("$") + output.printInt(fieldNameIdBuffer.toLong()) + output.appendAscii(":") + if (options.isPrettyPrintOn) output.appendAscii(" ") + fieldNameIdBuffer = -1 + } + } + + for (i in 0 until numAnnotations) { + if (annotationsTextBuffer[i] != null) { + output.printSymbol(annotationsTextBuffer[i]) + annotationsTextBuffer[i] = null + } else { + output.appendAscii("$") + output.printInt(annotationsIdBuffer[i].toLong()) + annotationsIdBuffer[i] = -1 + } + output.appendAscii("::") + } + + hasFieldName = false + numAnnotations = 0 + valueWriterExpression() + } + + private inline fun closeValue(valueWriterExpression: () -> Unit) { + valueWriterExpression() + isPendingSeparator = true + isPendingLeadingWhitespace = false + currentContainerHasValues = true + } + + private inline fun writeScalar(valueWriterExpression: () -> Unit) { + // Noteā€”it doesn't matter which order we combine these. The result will be the same because of where + // valueWriterExpression is called in openValue and closeValue. + openValue { closeValue(valueWriterExpression) } + } + + override fun close() { + if (closed) return + finish() + output.close() + closed = true + } + + override fun finish() { + if (closed) return + confirm(depth() == 0) { "Cannot call finish() while in a container" } + confirm(numAnnotations == 0) { "Cannot call finish with dangling annotations" } + } + + override fun writeIVM() { + confirm(currentContainer == Top) { "IVM can only be written at the top level of an Ion stream." } + confirm(numAnnotations == 0) { "Cannot write an IVM with annotations" } + output.appendAscii("\$ion_1_1") + isPendingSeparator = true + } + + override fun isInStruct(): Boolean = currentContainer == Struct + + override fun depth(): Int = ancestorContainersStack.size + + /** + * Ensures that there is enough space in the annotation buffers for [n] annotations. + * If more space is needed, it over-allocates by 8 to ensure that we're not continually allocating when annotations + * are being added one by one. + */ + private inline fun ensureAnnotationSpace(n: Int) { + // We only need to check the size of one of the arrays because we always keep them the same size. + if (annotationsIdBuffer.size < n) { + val oldIds = annotationsIdBuffer + annotationsIdBuffer = IntArray(n + 8) + oldIds.copyInto(annotationsIdBuffer) + val oldText = annotationsTextBuffer + annotationsTextBuffer = arrayOfNulls(n + 8) + oldText.copyInto(annotationsTextBuffer) + } + } + + override fun writeAnnotations(annotation0: Int) { + ensureAnnotationSpace(numAnnotations + 1) + annotationsIdBuffer[numAnnotations++] = annotation0 + } + + override fun writeAnnotations(annotation0: Int, annotation1: Int) { + ensureAnnotationSpace(numAnnotations + 2) + annotationsIdBuffer[numAnnotations++] = annotation0 + annotationsIdBuffer[numAnnotations++] = annotation1 + } + + override fun writeAnnotations(annotations: IntArray) { + ensureAnnotationSpace(numAnnotations + annotations.size) + annotations.copyInto(annotationsIdBuffer, numAnnotations) + numAnnotations += annotations.size + } + + override fun writeAnnotations(annotation0: CharSequence) { + ensureAnnotationSpace(numAnnotations + 1) + annotationsTextBuffer[numAnnotations++] = annotation0 + } + + override fun writeAnnotations(annotation0: CharSequence, annotation1: CharSequence) { + ensureAnnotationSpace(numAnnotations + 2) + annotationsTextBuffer[numAnnotations++] = annotation0 + annotationsTextBuffer[numAnnotations++] = annotation1 + } + + override fun writeAnnotations(annotations: Array) { + if (annotations.isEmpty()) return + ensureAnnotationSpace(numAnnotations + annotations.size) + annotations.copyInto(annotationsTextBuffer, numAnnotations) + numAnnotations += annotations.size + } + + override fun writeFieldName(sid: Int) { + confirm(currentContainer == Struct) { "Cannot write field name outside of a struct." } + confirm(!hasFieldName) { "Field name already set." } + fieldNameIdBuffer = sid + hasFieldName = true + } + + override fun writeFieldName(text: CharSequence) { + confirm(currentContainer == Struct) { "Cannot write field name outside of a struct." } + confirm(!hasFieldName) { "Field name already set." } + fieldNameTextBuffer = text + hasFieldName = true + } + + override fun writeNull() = writeScalar { + output.appendAscii("null") + } + + override fun writeNull(type: IonType) = writeScalar { + val nullimage = if (options._untyped_nulls) { "null" } else { + when (type) { + IonType.NULL -> "null" + IonType.BOOL -> "null.bool" + IonType.INT -> "null.int" + IonType.FLOAT -> "null.float" + IonType.DECIMAL -> "null.decimal" + IonType.TIMESTAMP -> "null.timestamp" + IonType.SYMBOL -> "null.symbol" + IonType.STRING -> "null.string" + IonType.BLOB -> "null.blob" + IonType.CLOB -> "null.clob" + IonType.SEXP -> "null.sexp" + IonType.LIST -> "null.list" + IonType.STRUCT -> "null.struct" + else -> throw IllegalStateException("unexpected type $type") + } + } + output.appendAscii(nullimage) + } + + override fun writeBool(value: Boolean) = writeScalar { output.appendAscii(if (value) "true" else "false") } + + override fun writeInt(value: Long) = writeScalar { output.printInt(value) } + override fun writeInt(value: BigInteger) = writeScalar { output.printInt(value) } + + override fun writeFloat(value: Float) = writeFloat(value.toDouble()) + override fun writeFloat(value: Double) = writeScalar { output.printFloat(options, value) } + + override fun writeDecimal(value: BigDecimal) = writeScalar { output.printDecimal(options, value) } + + override fun writeTimestamp(value: Timestamp) = writeScalar { + writeTimestampHelper( + toMillis = { value.millis }, + toString = { value.toString() }, + ) + } + + override fun writeTimestamp(value: Instant) = writeScalar { + writeTimestampHelper( + toMillis = { value.toEpochMilli() }, + toString = { value.toString() }, + ) + } + + private inline fun writeTimestampHelper(toMillis: () -> Long, toString: () -> String) { + if (options._timestamp_as_millis) { + output.appendAscii("${toMillis()}") + } else if (options._timestamp_as_string) { + // Timestamp is ASCII-safe so this is easy + output.appendAscii('"') + output.appendAscii(toString()) + output.appendAscii('"') + } else { + output.appendAscii(toString()) + } + } + + override fun writeSymbol(id: Int) = writeScalar { + output.appendAscii('$') + output.printInt(id.toLong()) + } + + override fun writeSymbol(text: CharSequence) = writeScalar { output.printSymbol(text) } + + override fun writeString(value: CharSequence) = writeScalar { output.printString(value) } + + override fun writeBlob(value: ByteArray, start: Int, length: Int) = writeScalar { output.printBlob(options, value, start, length) } + + override fun writeClob(value: ByteArray, start: Int, length: Int) = writeScalar { output.printClob(options, value, start, length) } + + override fun stepInList(delimited: Boolean) { + openValue { output.appendAscii("[") } + ancestorContainersStack.add(currentContainer) + currentContainer = List + currentContainerHasValues = false + isPendingLeadingWhitespace = true + } + + override fun stepInSExp(delimited: Boolean) { + openValue { output.appendAscii("(") } + ancestorContainersStack.add(currentContainer) + currentContainer = SExp + currentContainerHasValues = false + isPendingLeadingWhitespace = true + } + + override fun stepInStruct(delimited: Boolean) { + openValue { output.appendAscii("{") } + ancestorContainersStack.add(currentContainer) + currentContainer = Struct + currentContainerHasValues = false + isPendingLeadingWhitespace = true + } + + override fun stepInEExp(name: CharSequence) { + confirm(numAnnotations == 0) { "Cannot annotate a macro invocation" } + openValue { + output.appendAscii("(:") + output.printSymbol(name) + } + ancestorContainersStack.add(currentContainer) + currentContainer = Macro + currentContainerHasValues = false + isPendingSeparator = true // Treat the macro name as if it is a value that needs a separator. + } + + override fun stepInEExp(id: Int) { + confirm(numAnnotations == 0) { "Cannot annotate a macro invocation" } + openValue { + output.appendAscii("(:") + output.printInt(id.toLong()) + } + ancestorContainersStack.add(currentContainer) + currentContainer = Macro + currentContainerHasValues = false + isPendingSeparator = true // Treat the macro id as if it is a value that needs a separator. + } + + override fun stepInExpressionGroup(delimited: Boolean) { + confirm(numAnnotations == 0) { "Cannot annotate an expression group" } + confirm(currentContainer == Macro) { "Can only create an expression group in a macro invocation" } + openValue { output.appendAscii("[:") } + ancestorContainersStack.add(currentContainer) + currentContainer = ExpressionGroup + currentContainerHasValues = false + isPendingLeadingWhitespace = true + } + + override fun stepOut() { + confirm(numAnnotations == 0) { "Cannot step out with a dangling annotation" } + confirm(!hasFieldName) { "Cannot step out with a dangling field name" } + val endChar = when (currentContainer) { + Struct -> '}' + SExp, Macro -> ')' + List, ExpressionGroup -> ']' + Top -> throw IonException("Nothing to step out of.") + } + + currentContainer = ancestorContainersStack.removeLast() + + closeValue { + if (options.isPrettyPrintOn && currentContainerHasValues) { + output.appendAscii(options.lineSeparator()) + output.appendAscii(" ".repeat(ancestorContainersStack.size * 2)) + } + output.appendAscii(endChar) + } + } +} diff --git a/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/IonRawWriter_1_1.kt similarity index 99% rename from src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt rename to src/main/java/com/amazon/ion/impl/IonRawWriter_1_1.kt index ebc23d0dd6..709e22a04b 100644 --- a/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/IonRawWriter_1_1.kt @@ -11,7 +11,7 @@ import java.time.Instant * * This interface allows the user to write Ion data without being concerned about which output format is being used. */ -interface IonWriter_1_1 { +interface IonRawWriter_1_1 { /** * Indicates that writing is completed and all buffered data should be written and flushed as if this were the end diff --git a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt index 08d0238a11..39a2c35fa2 100644 --- a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt @@ -20,7 +20,7 @@ class IonRawBinaryWriter_1_1 internal constructor( private val out: ByteArrayOutputStream, private val buffer: WriteBuffer, private val lengthPrefixPreallocation: Int, -) : IonWriter_1_1 { +) : IonRawWriter_1_1 { /** * Types of encoding containers. diff --git a/src/test/java/com/amazon/ion/impl/IonRawTextWriterTest_1_1.kt b/src/test/java/com/amazon/ion/impl/IonRawTextWriterTest_1_1.kt new file mode 100644 index 0000000000..46018ef619 --- /dev/null +++ b/src/test/java/com/amazon/ion/impl/IonRawTextWriterTest_1_1.kt @@ -0,0 +1,1022 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.impl + +import com.amazon.ion.* +import com.amazon.ion.system.* +import java.math.BigDecimal +import java.math.BigInteger +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class IonRawTextWriterTest_1_1 { + + private inline fun writeAsString( + builderConfigurator: IonTextWriterBuilder.() -> Unit = { /* noop */ }, + autoClose: Boolean = true, + block: IonRawTextWriter_1_1.() -> Unit, + ): String { + val out = StringBuilder() + val b = IonTextWriterBuilder.standard() + .apply(builderConfigurator) + // Always use LF because the tests' expected data uses LF. + .withNewLineType(IonTextWriterBuilder.NewLineType.LF) + + val rawWriter = IonRawTextWriter_1_1( + options = b as _Private_IonTextWriterBuilder, + output = _Private_IonTextAppender.forAppendable(out) + ) + block.invoke(rawWriter) + if (autoClose) rawWriter.close() + return out.toString() + } + + private inline fun assertWriterOutputEquals( + text: String, + builderConfigurator: IonTextWriterBuilder.() -> Unit = { /* noop */ }, + autoClose: Boolean = true, + block: IonRawTextWriter_1_1.() -> Unit, + ) { + assertEquals(text, writeAsString(builderConfigurator, autoClose, block)) + } + + private inline fun assertWriterThrows(block: IonRawTextWriter_1_1.() -> Unit) { + val out = StringBuilder() + val rawWriter = IonRawTextWriter_1_1( + options = _Private_IonTextWriterBuilder.STANDARD, + output = _Private_IonTextAppender.forAppendable(out) + ) + assertThrows { + block.invoke(rawWriter) + } + } + + @Test + fun `calling close while in a container should throw IonException`() { + assertWriterThrows { + stepInList(false) + close() + } + } + + @Test + fun `calling finish while in a container should throw IonException`() { + assertWriterThrows { + stepInList(true) + finish() + } + } + + @Test + fun `calling finish with a dangling annotation should throw IonException`() { + assertWriterThrows { + writeAnnotations(10) + finish() + } + } + + @Test + fun `calling stepOut while not in a container should throw IonException`() { + assertWriterThrows { + stepOut() + } + } + + @Test + fun `calling stepOut with a dangling annotation should throw IonException`() { + assertWriterThrows { + stepInList(true) + writeAnnotations(10) + stepOut() + } + } + + @Test + fun `calling writeIVM when in a container should throw IonException`() { + assertWriterThrows { + stepInList(false) + writeIVM() + } + } + + @Test + fun `calling writeIVM with a dangling annotation should throw IonException`() { + assertWriterThrows { + writeAnnotations(10) + writeIVM() + } + } + + @Test + fun `calling finish should cause the buffered data to be written to the output stream`() { + val actual = writeAsString(autoClose = false) { + writeIVM() + finish() + } + // Just checking that data is written, not asserting the content. + assertTrue(actual.isNotBlank()) + } + + @Test + fun `after calling finish, it should still be possible to write more data`() { + val actual = writeAsString { + finish() + writeIVM() + } + // Just checking that data is written, not asserting the content. + assertTrue(actual.isNotBlank()) + } + + @Test + fun `calling close should cause the buffered data to be written to the output stream`() { + val actual = writeAsString(autoClose = false) { + writeIVM() + close() + } + // Just checking that data is written, not asserting the content. + assertTrue(actual.isNotBlank()) + } + + @Test + fun `calling close or finish multiple times should not throw any exceptions`() { + val actual = writeAsString { + writeIVM() + finish() + close() + finish() + close() + finish() + } + // Just checking that data is written, not asserting the content. + assertTrue(actual.isNotBlank()) + } + + @Test + fun `write the IVM`() { + assertWriterOutputEquals("\$ion_1_1") { + writeIVM() + } + } + + @Test + fun `write nothing`() { + assertWriterOutputEquals("") { + } + } + + @Test + fun `write a null`() { + assertWriterOutputEquals("null") { + writeNull() + } + } + + @Test + fun `write a null with a specific type`() { + // Just checking one type. The full range of types are checked in IonEncoder_1_1Test + assertWriterOutputEquals("null.bool") { + writeNull(IonType.BOOL) + } + } + + @ParameterizedTest + @CsvSource("true, true", "false, false") + fun `write a boolean`(value: Boolean, expected: String) { + assertWriterOutputEquals(expected) { + writeBool(value) + } + } + + @Test + fun `write a delimited list`() { + assertWriterOutputEquals("[true,false]") { + stepInList(true) + writeBool(true) + writeBool(false) + stepOut() + } + } + + @Test + fun `write a prefixed list`() { + assertWriterOutputEquals("[true,false]") { + stepInList(false) + writeBool(true) + writeBool(false) + stepOut() + } + } + + @Test + fun `write multiple nested prefixed lists`() { + assertWriterOutputEquals("[[[[[]]]]]") { + repeat(5) { stepInList(false) } + repeat(5) { stepOut() } + } + } + + @Test + fun `write multiple nested delimited lists`() { + assertWriterOutputEquals("[[[[]]]]") { + repeat(4) { stepInList(true) } + repeat(4) { stepOut() } + } + } + + @Test + fun `write multiple nested delimited and prefixed lists`() { + assertWriterOutputEquals("[[[[[[[[]]]]]]]]") { + repeat(4) { + stepInList(true) + stepInList(false) + } + repeat(8) { stepOut() } + } + } + + @Test + fun `write a sexp`() { + assertWriterOutputEquals("(true false)") { + stepInSExp(delimited = true) + writeBool(true) + writeBool(false) + stepOut() + } + assertWriterOutputEquals("(true false)") { + stepInSExp(delimited = false) + writeBool(true) + writeBool(false) + stepOut() + } + } + + @Test + fun `write multiple nested sexps`() { + assertWriterOutputEquals("(((((((())))))))") { + repeat(4) { + stepInSExp(delimited = true) + stepInSExp(delimited = false) + } + repeat(8) { stepOut() } + } + } + + @Test + fun `write a struct`() { + assertWriterOutputEquals( + """{$11:true,$12:false}""" + ) { + stepInStruct(delimited = false) + writeFieldName(11) + writeBool(true) + writeFieldName(12) + writeBool(false) + stepOut() + } + assertWriterOutputEquals( + """{$11:true,$12:false}""" + ) { + stepInStruct(delimited = true) + writeFieldName(11) + writeBool(true) + writeFieldName(12) + writeBool(false) + stepOut() + } + } + + @Test + fun `write multiple nested structs`() { + assertWriterOutputEquals( + "{a:{b:{a:{b:{a:{b:{a:{b:{}}}}}}}}}" + ) { + stepInStruct(delimited = false) + repeat(4) { + writeFieldName("a") + stepInStruct(delimited = true) + writeFieldName("b") + stepInStruct(delimited = false) + } + repeat(9) { + stepOut() + } + } + } + + @Test + fun `write empty struct`() { + assertWriterOutputEquals("{}") { + stepInStruct(delimited = false) + stepOut() + } + assertWriterOutputEquals("{}") { + stepInStruct(delimited = true) + stepOut() + } + } + + @Test + fun `write prefixed struct with a single flex sym field`() { + assertWriterOutputEquals( + """{foo:true}""" + ) { + stepInStruct(false) + writeFieldName("foo") + writeBool(true) + stepOut() + } + } + + @Test + fun `write prefixed struct with multiple fields and flex syms`() { + assertWriterOutputEquals( + """{foo:true,bar:true,baz:true}""" + ) { + stepInStruct(false) + writeFieldName("foo") + writeBool(true) + writeFieldName("bar") + writeBool(true) + writeFieldName("baz") + writeBool(true) + stepOut() + } + } + + @Test + fun `write a struct with sid 0`() { + assertWriterOutputEquals( + "{\$0:true}" + ) { + stepInStruct(delimited = false) + writeFieldName(0) + writeBool(true) + stepOut() + } + assertWriterOutputEquals( + "{\$0:true}" + ) { + stepInStruct(delimited = true) + writeFieldName(0) + writeBool(true) + stepOut() + } + } + + @Test + fun `writing a value in a struct with no field name should throw an exception`() { + assertWriterThrows { + stepInStruct(true) + writeBool(true) + } + assertWriterThrows { + stepInStruct(false) + writeBool(true) + } + } + + @Test + fun `calling writeFieldName outside of a struct should throw an exception`() { + assertWriterThrows { + writeFieldName(12) + } + assertWriterThrows { + writeFieldName("foo") + } + } + + @Test + fun `calling stepOut with a dangling field name should throw an exception`() { + assertWriterThrows { + stepInStruct(false) + writeFieldName(12) + stepOut() + } + assertWriterThrows { + stepInStruct(true) + writeFieldName("foo") + stepOut() + } + } + + @Test + fun `writeAnnotations with empty int array should write no annotations`() { + assertWriterOutputEquals("true") { + writeAnnotations(intArrayOf()) + writeBool(true) + } + } + + @Test + fun `write one sid annotation`() { + val expectedBytes = "\$3::true" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(intArrayOf()) + writeAnnotations(arrayOf()) + writeBool(true) + } + } + + @Test + fun `write two sid annotations`() { + val expectedBytes = "\$3::\$4::true" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(4) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3, 4) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(intArrayOf(3, 4)) + writeBool(true) + } + } + + @Test + fun `write three sid annotations`() { + val expectedBytes = "\$3::\$4::\$256::true" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(4) + writeAnnotations(256) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(4, 256) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(intArrayOf(3, 4)) + writeAnnotations(256) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(intArrayOf(3, 4, 256)) + writeBool(true) + } + } + + @Test + fun `write sid 0 annotation`() { + assertWriterOutputEquals("\$0::true") { + writeAnnotations(0) + writeBool(true) + } + } + + @Test + fun `write one inline annotation`() { + val expectedBytes = "foo::false" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations(intArrayOf()) + writeBool(false) + } + } + + @Test + fun `write two text annotations`() { + val expectedBytes = "foo::bar::false" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations("bar") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(arrayOf("foo", "bar")) + writeBool(false) + } + } + + @Test + fun `write three text annotations`() { + val expectedBytes = "foo::bar::baz::false" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations("bar") + writeAnnotations("baz") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations("bar", "baz") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(arrayOf("foo", "bar")) + writeAnnotations("baz") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(arrayOf("foo", "bar", "baz")) + writeBool(false) + } + } + + @Test + fun `write empty text and sid 0 annotations`() { + assertWriterOutputEquals("\$0::''::true") { + writeAnnotations(0) + writeAnnotations("") + writeBool(true) + } + } + + @Test + fun `write two mixed sid and text annotations`() { + val expectedBytes = "\$10::foo::false" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(10) + writeAnnotations("foo") + writeBool(false) + } + } + + @Test + fun `write three mixed sid and inline annotations`() { + val expectedBytes = "\$10::foo::bar::false" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(10) + writeAnnotations("foo") + writeAnnotations("bar") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(10) + writeAnnotations(arrayOf("foo", "bar")) + writeBool(false) + } + } + + @Test + fun `write int`() { + assertWriterOutputEquals( + """1 10""" + ) { + writeInt(1) + writeInt(BigInteger.TEN) + } + } + + @Test + fun `write float`() { + assertWriterOutputEquals( + """0e0 3.140000104904175e0 3.14e0""" + ) { + writeFloat(0.0) + writeFloat(3.14f) + writeFloat(3.14) + } + } + + @Test + fun `write decimal`() { + assertWriterOutputEquals( + """0. -0.""" + ) { + writeDecimal(BigDecimal.ZERO) + writeDecimal(Decimal.NEGATIVE_ZERO) + } + } + + @Test + fun `write timestamp`() { + assertWriterOutputEquals( + """2023-12-08T15:37:23.190583253Z 2123T""" + ) { + writeTimestamp(Timestamp.valueOf("2023-12-08T15:37:23.190583253Z")) + writeTimestamp(Timestamp.valueOf("2123T")) + } + } + + @Test + fun `write symbol`() { + assertWriterOutputEquals( + "\$0 \$1 \$12345 foo 'null' 'bat\\'leth'" + ) { + writeSymbol(0) + writeSymbol(1) + writeSymbol(12345) + writeSymbol("foo") + writeSymbol("null") + writeSymbol("bat'leth") + } + } + + @Test + fun `write string`() { + assertWriterOutputEquals("\"foo\"") { + writeString("foo") + } + } + + @Test + fun `write blob`() { + assertWriterOutputEquals("{{AQID}}") { + writeBlob(byteArrayOf(1, 2, 3), 0, 3) + } + } + + @Test + fun `write clob`() { + assertWriterOutputEquals("{{\"abc\"}}") { + writeClob(byteArrayOf(0x61, 0x62, 0x63), 0, 3) + } + } + + @Test + fun `write E-expression by name`() { + assertWriterOutputEquals("(:foo)") { + stepInEExp("foo") + stepOut() + } + assertWriterOutputEquals("(:'1A')") { + stepInEExp("1A") + stepOut() + } + } + + @Test + fun `write E-expression by id`() { + assertWriterOutputEquals("(:1)") { + stepInEExp(1) + stepOut() + } + } + + @Test + fun `write E-Expression with one arg`() { + assertWriterOutputEquals("(:foo true)") { + stepInEExp("foo") + writeBool(true) + stepOut() + } + } + + @Test + fun `write an expression group`() { + assertWriterOutputEquals("(:foo [:true,true] [:false,false])") { + writeEExp("foo") { + writeExpressionGroup { + writeBool(true) + writeBool(true) + } + // Can't use writeExpressionGroup for this because it sets delimited = true + stepInExpressionGroup(delimited = false) + writeBool(false) + writeBool(false) + stepOut() + } + } + } + + @Test + fun `write an empty expression group`() { + assertWriterOutputEquals("(:foo [:])") { + writeEExp("foo") { + stepInExpressionGroup(false) + stepOut() + } + } + } + + @Test + fun `calling stepInExpressionGroup with an annotation should throw IonException`() { + assertWriterThrows { + stepInEExp(1) + writeAnnotations("foo") + stepInExpressionGroup(false) + } + } + + @Test + fun `calling stepInExpressionGroup while not directly in a Macro container should throw IonException`() { + assertWriterThrows { stepInExpressionGroup(true) } + assertWriterThrows { writeList { stepInExpressionGroup(true) } } + assertWriterThrows { writeSexp { stepInExpressionGroup(true) } } + assertWriterThrows { writeStruct { stepInExpressionGroup(true) } } + assertWriterThrows { writeEExp(123) { writeExpressionGroup { stepInExpressionGroup(true) } } } + } + + /** + * Writes this Ion, taken from https://amazon-ion.github.io/ion-docs/ + * ``` + * { + * name: "Fido", + * age: years::4, + * birthday: 2012-03-01T, + * toys: [ball, rope], + * weight: pounds::41.2, + * buzz: {{VG8gaW5maW5pdHkuLi4gYW5kIGJleW9uZCE=}}, + * } + * ``` + */ + @Test + fun `write something complex with symtab`() { + assertWriterOutputEquals( + """${'$'}ion_1_1 $3::{$7:["name","age","years","birthday","toys","ball","weight","buzz"]} {$10:"Fido",$11:$12::4,$13:2012-03-01,$14:[$15,rope],$16:pounds::41.2,$17:{{VG8gaW5maW5pdHkuLi4gYW5kIGJleW9uZCE=}}}""" + ) { + writeIVM() + writeAnnotations(3) + writeStruct { + writeFieldName(7) + writeList { + writeString("name") + writeString("age") + writeString("years") + writeString("birthday") + writeString("toys") + writeString("ball") + writeString("weight") + writeString("buzz") + } + } + writeStruct { + writeFieldName(10) + writeString("Fido") + writeFieldName(11) + writeAnnotations(12) + writeInt(4) + writeFieldName(13) + writeTimestamp(Timestamp.valueOf("2012-03-01T")) + writeFieldName(14) + writeList { + writeSymbol(15) + writeSymbol("rope") + } + writeFieldName(16) + writeAnnotations("pounds") + writeDecimal(BigDecimal.valueOf(41.2)) + writeFieldName(17) + writeBlob( + byteArrayOf( + 84, 111, 32, 105, 110, 102, 105, 110, 105, + 116, 121, 46, 46, 46, 32, 97, 110, 100, + 32, 98, 101, 121, 111, 110, 100, 33 + ) + ) + } + } + } + + @Test + fun `write something complex`() { + assertWriterOutputEquals( + """${'$'}ion_1_1 {name:"Fido",age:years::4,birthday:2012-03-01,toys:[ball,rope],weight:pounds::41.2,buzz:{{VG8gaW5maW5pdHkuLi4gYW5kIGJleW9uZCE=}}}""" + ) { + writeIVM() + writeStruct { + writeFieldName("name") + writeString("Fido") + writeFieldName("age") + writeAnnotations("years") + writeInt(4) + writeFieldName("birthday") + writeTimestamp(Timestamp.valueOf("2012-03-01T")) + writeFieldName("toys") + writeList { + writeSymbol("ball") + writeSymbol("rope") + } + writeFieldName("weight") + writeAnnotations("pounds") + writeDecimal(BigDecimal.valueOf(41.2)) + writeFieldName("buzz") + writeBlob( + byteArrayOf( + 84, 111, 32, 105, 110, 102, 105, 110, 105, + 116, 121, 46, 46, 46, 32, 97, 110, 100, + 32, 98, 101, 121, 111, 110, 100, 33 + ) + ) + } + } + } + + @Test + fun `write something complex and pretty`() { + val expected = """ + ${'$'}ion_1_1 + { + name: "Fido", + age: years::4, + birthday: 2012-03-01, + toys: [ + ball, + rope + ], + weight: pounds::41.2, + buzz: {{ VG8gaW5maW5pdHkuLi4gYW5kIGJleW9uZCE= }} + } + """.trimIndent() + assertWriterOutputEquals( + text = expected, + builderConfigurator = { withPrettyPrinting() } + ) { + writeIVM() + writeStruct { + writeFieldName("name") + writeString("Fido") + writeFieldName("age") + writeAnnotations("years") + writeInt(4) + writeFieldName("birthday") + writeTimestamp(Timestamp.valueOf("2012-03-01T")) + writeFieldName("toys") + writeList { + writeSymbol("ball") + writeSymbol("rope") + } + writeFieldName("weight") + writeAnnotations("pounds") + writeDecimal(BigDecimal.valueOf(41.2)) + writeFieldName("buzz") + writeBlob( + byteArrayOf( + 84, 111, 32, 105, 110, 102, 105, 110, 105, + 116, 121, 46, 46, 46, 32, 97, 110, 100, + 32, 98, 101, 121, 111, 110, 100, 33 + ) + ) + } + } + } + + @Test + fun `write something complex and compact`() { + val expected = """ + ${'$'}ion_1_1 + {name:"Fido",age:years::4,birthday:2012-03-01,toys:[ball,rope],weight:pounds::41.2,buzz:{{VG8gaW5maW5pdHkuLi4gYW5kIGJleW9uZCE=}}} + {name:"Rufus",age:years::5,birthday:2012-03-02,toys:[textbook],weight:pounds::98.5} + """.trimIndent() + assertWriterOutputEquals( + text = expected, + builderConfigurator = { withWriteTopLevelValuesOnNewLines(true) } + ) { + writeIVM() + writeStruct { + writeFieldName("name") + writeString("Fido") + writeFieldName("age") + writeAnnotations("years") + writeInt(4) + writeFieldName("birthday") + writeTimestamp(Timestamp.valueOf("2012-03-01T")) + writeFieldName("toys") + writeList { + writeSymbol("ball") + writeSymbol("rope") + } + writeFieldName("weight") + writeAnnotations("pounds") + writeDecimal(BigDecimal.valueOf(41.2)) + writeFieldName("buzz") + writeBlob( + byteArrayOf( + 84, 111, 32, 105, 110, 102, 105, 110, 105, + 116, 121, 46, 46, 46, 32, 97, 110, 100, + 32, 98, 101, 121, 111, 110, 100, 33 + ) + ) + } + writeStruct { + writeFieldName("name") + writeString("Rufus") + writeFieldName("age") + writeAnnotations("years") + writeInt(5) + writeFieldName("birthday") + writeTimestamp(Timestamp.valueOf("2012-03-02T")) + writeFieldName("toys") + writeList { + writeSymbol("textbook") + } + writeFieldName("weight") + writeAnnotations("pounds") + writeDecimal(BigDecimal.valueOf(98.5)) + } + } + } + + @Test + fun `write something pretty with a macro`() { + val expected = """ + ${'$'}ion_1_1 + { + name: (:make_string + "F" + "ido" + ) + } + """.trimIndent() + assertWriterOutputEquals( + text = expected, + builderConfigurator = { withPrettyPrinting() } + ) { + writeIVM() + writeStruct { + writeFieldName("name") + stepInEExp("make_string") + writeString("F") + writeString("ido") + stepOut() + } + } + } + + @Test + fun `when pretty printing, empty containers should be on one line`() { + val expected = """ + ${'$'}ion_1_1 + { + a: {} + } + [ + [] + ] + ( + () + ) + (:foo + (:foo) + ) + (:1 + (:1) + ) + """.trimIndent() + assertWriterOutputEquals( + text = expected, + builderConfigurator = { withPrettyPrinting() } + ) { + writeIVM() + writeStruct { + writeFieldName("a") + stepInStruct(false); stepOut() + } + writeList { writeList { } } + writeSexp { writeSexp { } } + writeEExp("foo") { writeEExp("foo") { } } + writeEExp(1) { writeEExp(1) { } } + } + } + + /* + * Helper functions that steps into a container, applies the contents of [block] to + * the writer, and then steps out of that container. + * Using these functions makes it easy for the indentation of the writer code to + * match the indentation of the equivalent pretty-printed Ion. + */ + + private inline fun IonRawWriter_1_1.writeStruct(block: IonRawWriter_1_1.() -> Unit) { + stepInStruct(true) + block() + stepOut() + } + + private inline fun IonRawWriter_1_1.writeList(block: IonRawWriter_1_1.() -> Unit) { + stepInList(true) + block() + stepOut() + } + + private inline fun IonRawWriter_1_1.writeSexp(block: IonRawWriter_1_1.() -> Unit) { + stepInSExp(true) + block() + stepOut() + } + + private inline fun IonRawWriter_1_1.writeEExp(name: String, block: IonRawWriter_1_1.() -> Unit) { + stepInEExp(name) + block() + stepOut() + } + + private inline fun IonRawWriter_1_1.writeEExp(id: Int, block: IonRawWriter_1_1.() -> Unit) { + stepInEExp(id) + block() + stepOut() + } + + private inline fun IonRawWriter_1_1.writeExpressionGroup(block: IonRawWriter_1_1.() -> Unit) { + stepInExpressionGroup(true) + block() + stepOut() + } +} diff --git a/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt b/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt index 650559d824..6c9c7b2478 100644 --- a/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt +++ b/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt @@ -1253,7 +1253,7 @@ class IonRawBinaryWriterTest_1_1 { * Using this function makes it easy for the indentation of the writer code to * match the indentation of the equivalent pretty-printed Ion. */ - private inline fun IonWriter_1_1.writeStruct(block: IonWriter_1_1.() -> Unit) { + private inline fun IonRawWriter_1_1.writeStruct(block: IonRawWriter_1_1.() -> Unit) { stepInStruct(false) block() stepOut() @@ -1265,7 +1265,7 @@ class IonRawBinaryWriterTest_1_1 { * Using this function makes it easy for the indentation of the writer code to * match the indentation of the equivalent pretty-printed Ion. */ - private inline fun IonWriter_1_1.writeList(block: IonWriter_1_1.() -> Unit) { + private inline fun IonRawWriter_1_1.writeList(block: IonRawWriter_1_1.() -> Unit) { stepInList(false) block() stepOut()