Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/main/java/com/amazon/ion/IonWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion;

import com.amazon.ion.eexp.EExpression;
import com.amazon.ion.facet.Faceted;
import com.amazon.ion.system.IonTextWriterBuilder;
import com.amazon.ion.util.IonStreamUtils;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.Flushable;
Expand Down Expand Up @@ -204,6 +206,7 @@ public interface IonWriter
*/
public void setFieldNameSymbol(SymbolToken name);

public boolean isFieldNameSet();

/**
* Sets the full list of pending annotations to the given text symbols.
Expand Down Expand Up @@ -280,6 +283,8 @@ public interface IonWriter
public void stepOut() throws IOException;


public int getDepth();

/**
* Determines whether values are being written as fields of a struct.
* This is especially useful when it is not clear whether field names need
Expand Down Expand Up @@ -509,4 +514,8 @@ public void writeBlob(byte[] value, int start, int len)
public default void writeObject(WriteAsIon obj) {
obj.writeTo(this);
}

public default void writeEExpression(EExpression eExpression) {
throw new UnsupportedOperationException("writeEExpression not supported by Ion 1.0 writers.");
}
}
7 changes: 5 additions & 2 deletions src/main/java/com/amazon/ion/MacroAwareIonWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import com.amazon.ion.impl.macro.*
* Extension of the IonWriter interface that supports writing macros.
*
* TODO: Consider exposing this as a Facet.
*
* TODO: See if we can have some sort of safe reference to a macro.
*/
interface MacroAwareIonWriter : IonWriter {

/**
* TODO: This should be internal-only, I think.
*
* Starts a new encoding segment with an Ion version marker, flushing
* the previous segment (if any) and resetting the encoding context.
*/
fun startEncodingSegmentWithIonVersionMarker()

/**
*
* TODO: This should be internal-only, I think.
*
* Starts a new encoding segment with an encoding directive, flushing
* the previous segment (if any).
* @param macros the macros added in the new segment.
Expand Down
58 changes: 56 additions & 2 deletions src/main/java/com/amazon/ion/WriteAsIon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,69 @@
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion

import com.amazon.ion.eexp.*
import com.amazon.ion.impl.macro.*

/**
* Indicates that the implementing class has a standardized/built-in way to serialize as Ion.
*
* Example implementation:
*
* ```kotlin
* data class Point2D(val x: Long, val y: Long) : WriteAsIon {
* companion object {
* // This is a very long macro name, but by using the qualified class name,
* // there is almost no risk of having a name conflict with another macro.
* private val MACRO_NAME = Point2D::class.simpleName!!.replace(".", "_")
* private val MACRO = TemplateMacro(
* signature = listOf(exactlyOneTagged("x"), exactlyOneTagged("y")),
* templateBody {
* struct {
* fieldName("x"); variable(0)
* fieldName("y"); variable(1)
* }
* }
* )
* }
*
* override fun writeWithEExpression(builder: EExpressionBuilder): EExpression? {
* return builder.withName(MACRO_NAME)
* .withMacro(MACRO)
* .withIntArgument(x)
* .withIntArgument(y)
* .build()
* }
*
* override fun writeTo(writer: IonWriter) {
* with(writer) {
* stepIn(IonType.STRUCT)
* setFieldName("x"); writeInt(x)
* setFieldName("x"); writeInt(y)
* stepOut()
* }
* }
* }
* ```
*
* TODO: There is a significant weakness in this API—if someone calls `myObject.writeTo(myWriter)`, then the check for
* e-expression support is completely bypassed.
*/
interface WriteAsIon {

/**
* Writes this object to an IonWriter capable of producing macro invocations.
* Writes this value to an IonWriter using an E-Expression.
*
* Implementations must return an instance of [EExpression] to indicate that it can be written using an e-expression.
* Returning `null` indicates that this implementation or instance should not be serialized
*
* [EExpression] instances can be obtained from an [EExpressionArgumentBuilder], which in turn, can be obtained by
* calling [Macro.createInvocation] or by using the supplied [builder] in this method.
*
* If you call any methods on [builder], but do not return the [EExpression] produced by [builder], something bad
* will happen. If you're lucky, an exception will be thrown. If you're unlucky, your application will continue
* running and produce invalid or incorrect data.
*/
fun writeToMacroAware(writer: MacroAwareIonWriter) = writeTo(writer as IonWriter)
fun writeWithEExpression(builder: EExpressionBuilder): EExpression? = null

/**
* Writes this object to a standard [IonWriter].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion.eexp

import com.amazon.ion.*
import com.amazon.ion.impl.*
import com.amazon.ion.impl.macro.*
import java.math.BigDecimal
import java.math.BigInteger
import java.util.Date

/**
* A decorator for IonWriter that validates arguments against macro parameter specifications.
*
* This class wraps an IonWriter and ensures that all values written through it conform to
* the constraints specified by a macro parameter.
*/
class ArgumentValidatingIonWriterDecorator(
private val parameter: Macro.Parameter,
private val delegate: IonWriter,
) : IonWriter {

private val encoding: Macro.ParameterEncoding = parameter.type

override fun close() {}
override fun flush() {}
override fun finish() {}
override fun <T : Any?> asFacet(facetType: Class<T>?): T? = null
override fun getSymbolTable(): SymbolTable? = delegate.symbolTable

var numberOfExpressions = 0
private set

private var depth = 0

override fun setFieldName(name: String?) = delegate.setFieldName(name)
override fun setFieldNameSymbol(name: SymbolToken?) = delegate.setFieldNameSymbol(name)
override fun isFieldNameSet(): Boolean = delegate.isFieldNameSet

override fun setTypeAnnotations(vararg annotations: String?) {
if (depth == 0 && annotations.isNotEmpty()) require(encoding == Macro.ParameterEncoding.Tagged) { "Parameter with encoding $encoding cannot be annotated." }
delegate.setTypeAnnotations(*annotations)
}

override fun setTypeAnnotationSymbols(vararg annotations: SymbolToken?) {
if (depth == 0 && annotations.isNotEmpty()) require(encoding == Macro.ParameterEncoding.Tagged) { "Parameter with encoding $encoding cannot be annotated." }
delegate.setTypeAnnotationSymbols(*annotations)
}

override fun addTypeAnnotation(annotation: String?) {
if (depth == 0) require(encoding == Macro.ParameterEncoding.Tagged) { "Parameter with encoding $encoding cannot be annotated." }
delegate.addTypeAnnotation(annotation)
}

override fun stepIn(containerType: IonType?) {
when (containerType) {
IonType.LIST, IonType.SEXP, IonType.STRUCT -> {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "Type $containerType is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.stepIn(containerType)
depth++
}
else -> throw IonException("Not a container type: $containerType")
}
}

override fun stepOut() {
if (depth == 0) throw IonException("Nothing to step out from")
depth--
}

override fun getDepth(): Int = depth

override fun isInStruct(): Boolean = delegate.isInStruct()

@Deprecated("Deprecated in IonWriter", ReplaceWith("value.writeTo(this)"))
override fun writeValue(value: IonValue?) {
value?.writeTo(this)
}

override fun writeValue(reader: IonReader) {
DefaultReaderToWriterTransfer.writeValue(reader, this)
}

override fun writeValues(reader: IonReader) {
if (reader.type == null) reader.next()
while (reader.type != null) {
writeValue(reader)
reader.next()
}
}

override fun writeNull() {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "null value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeNull()
}

override fun writeNull(type: IonType?) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "null.${type.toString().lowercase()} value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeNull(type)
}

override fun writeBool(value: Boolean) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "bool value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeBool(value)
}

override fun writeInt(value: Long) {
if (depth == 0) {
checkArgumentEncodingCompatibility(parameter, value)
numberOfExpressions++
}
delegate.writeInt(value)
}

override fun writeInt(value: BigInteger?) {
if (depth == 0) {
checkArgumentEncodingCompatibility(parameter, value)
numberOfExpressions++
}
delegate.writeInt(value)
}

override fun writeFloat(value: Double) {
if (depth == 0) {
checkArgumentEncodingCompatibility(parameter, value)
numberOfExpressions++
}
delegate.writeFloat(value)
}

override fun writeDecimal(value: BigDecimal?) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "decimal value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeDecimal(value)
}

override fun writeTimestamp(value: Timestamp?) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "timestamp value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeTimestamp(value)
}

override fun writeTimestampUTC(value: Date?) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "timestamp value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeTimestampUTC(value)
}

override fun writeSymbol(content: String?) {
if (depth == 0) {
when (encoding) {
Macro.ParameterEncoding.Tagged -> delegate.writeSymbol(content)
Macro.ParameterEncoding.FlexString -> {
if (content == null) throw IllegalArgumentException("null.symbol is not valid for parameter encoding $encoding")
delegate.writeSymbol(content)
}
else -> throw IllegalArgumentException("symbol value is not valid for parameter encoding $encoding")
}
numberOfExpressions++
}
delegate.writeSymbol(content)
}

override fun writeSymbolToken(content: SymbolToken?) {
if (depth == 0) {
when (encoding) {
Macro.ParameterEncoding.Tagged -> delegate.writeSymbolToken(content)
Macro.ParameterEncoding.FlexString -> {
if (content == null) throw IllegalArgumentException("null.symbol is not valid for parameter encoding $encoding")
TODO()
}
else -> throw IllegalArgumentException("symbol value is not valid for parameter encoding $encoding")
}
numberOfExpressions++
}
delegate.writeSymbolToken(content)
}

override fun writeString(value: String?) {
if (depth == 0) {
checkArgumentEncodingCompatibility(parameter, value)
numberOfExpressions++
}
delegate.writeString(value)
}

override fun writeClob(value: ByteArray?) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "clob value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeClob(value)
}

override fun writeClob(value: ByteArray?, start: Int, len: Int) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "clob value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeClob(value, start, len)
}

override fun writeBlob(value: ByteArray?) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "blob value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeBlob(value)
}

override fun writeBlob(value: ByteArray?, start: Int, len: Int) {
if (depth == 0) {
require(encoding == Macro.ParameterEncoding.Tagged) { "blob value is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeBlob(value, start, len)
}

override fun writeObject(obj: WriteAsIon?) {
if (depth == 0) {
// TODO: How to correctly validate this? Can we defer it until we implement macro-shaped parameters?
// require(encoding == Macro.ParameterEncoding.Tagged) { "e-expression is not valid for parameter encoding $encoding." }
numberOfExpressions++
}
delegate.writeObject(obj)
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/amazon/ion/eexp/DirectEExpression.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion.eexp

/**
* This object serves as a sentinel/nonce type to help users of the library correctly
* use the Argument Builder APIs. It enforces type safety and proper usage patterns
* in the fluent builder API for direct encoding expressions.
*
* See [DirectEExpressionArgumentBuilder] and [WriteAsIon][com.amazon.ion.WriteAsIon]
*/
object DirectEExpression : EExpression
Loading
Loading