diff --git a/openai-java-core/src/main/kotlin/com/openai/core/Values.kt b/openai-java-core/src/main/kotlin/com/openai/core/Values.kt index 4003d35a..196140a5 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/Values.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/Values.kt @@ -29,30 +29,51 @@ import java.io.InputStream import java.util.Objects import java.util.Optional +/** + * A class representing a serializable JSON field. + * + * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an + * arbitrary JSON value that bypasses the type system (via [JsonValue]). + */ @JsonDeserialize(using = JsonField.Deserializer::class) sealed class JsonField { + /** + * Returns whether this field is missing, which means it will be omitted from the serialized + * JSON entirely. + */ fun isMissing(): Boolean = this is JsonMissing + /** Whether this field is explicitly set to `null`. */ fun isNull(): Boolean = this is JsonNull - fun asKnown(): Optional = - when (this) { - is KnownValue -> Optional.of(value) - else -> Optional.empty() - } + /** + * Returns an [Optional] containing this field's "known" value, meaning it matches the type the + * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue]. + * + * This is the opposite of [asUnknown]. + */ + fun asKnown(): + Optional< + // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that. + @UnsafeVariance + T + > = Optional.ofNullable((this as? KnownValue)?.value) /** - * If the "known" value (i.e. matching the type that the SDK expects) is returned by the API - * then this method will return an empty `Optional`, otherwise the returned `Optional` is given - * a `JsonValue`. + * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches + * the type the SDK expects, or an empty [Optional] if this field contains a "known" value. + * + * This is the opposite of [asKnown]. */ - fun asUnknown(): Optional = - when (this) { - is JsonValue -> Optional.of(this) - else -> Optional.empty() - } + fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue) + /** + * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it + * doesn't contain a boolean. + * + * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean]. + */ fun asBoolean(): Optional = when (this) { is JsonBoolean -> Optional.of(value) @@ -60,6 +81,12 @@ sealed class JsonField { else -> Optional.empty() } + /** + * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it + * doesn't contain a number. + * + * This method checks for both a [KnownValue] containing a number and for [JsonNumber]. + */ fun asNumber(): Optional = when (this) { is JsonNumber -> Optional.of(value) @@ -67,6 +94,12 @@ sealed class JsonField { else -> Optional.empty() } + /** + * Returns an [Optional] containing this field's string value, or an empty [Optional] if it + * doesn't contain a string. + * + * This method checks for both a [KnownValue] containing a string and for [JsonString]. + */ fun asString(): Optional = when (this) { is JsonString -> Optional.of(value) @@ -77,6 +110,12 @@ sealed class JsonField { fun asStringOrThrow(): String = asString().orElseThrow { OpenAIInvalidDataException("Value is not a string") } + /** + * Returns an [Optional] containing this field's list value, or an empty [Optional] if it + * doesn't contain a list. + * + * This method checks for both a [KnownValue] containing a list and for [JsonArray]. + */ fun asArray(): Optional> = when (this) { is JsonArray -> Optional.of(values) @@ -95,6 +134,12 @@ sealed class JsonField { else -> Optional.empty() } + /** + * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't + * contain a map. + * + * This method checks for both a [KnownValue] containing a map and for [JsonObject]. + */ fun asObject(): Optional> = when (this) { is JsonObject -> Optional.of(values) @@ -132,7 +177,13 @@ sealed class JsonField { } @JvmSynthetic - internal fun getOptional(name: String): Optional<@UnsafeVariance T> = + internal fun getOptional( + name: String + ): Optional< + // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that. + @UnsafeVariance + T + > = when (this) { is KnownValue -> Optional.of(value) is JsonMissing, @@ -147,21 +198,33 @@ sealed class JsonField { is JsonValue -> this } - @JvmSynthetic fun accept(consume: (T) -> Unit) = asKnown().ifPresent(consume) + @JvmSynthetic internal fun accept(consume: (T) -> Unit) = asKnown().ifPresent(consume) + /** Returns the result of calling the [visitor] method corresponding to this field's state. */ fun accept(visitor: Visitor): R = when (this) { is KnownValue -> visitor.visitKnown(value) is JsonValue -> accept(visitor as JsonValue.Visitor) } + /** + * An interface that defines how to map each possible state of a `JsonField` to a value of + * type [R]. + */ interface Visitor : JsonValue.Visitor { + fun visitKnown(value: T): R = visitDefault() } companion object { + + /** Returns a [JsonField] containing the given "known" [value]. */ @JvmStatic fun of(value: T): JsonField = KnownValue.of(value) + /** + * Returns a [JsonField] containing the given "known" [value], or [JsonNull] if [value] is + * null. + */ @JvmStatic fun ofNullable(value: T?): JsonField = when (value) { @@ -176,6 +239,7 @@ sealed class JsonField { * annotation. */ class IsMissing { + override fun equals(other: Any?): Boolean = other is JsonMissing override fun hashCode(): Int = Objects.hash() @@ -197,6 +261,12 @@ sealed class JsonField { } } +/** + * A class representing an arbitrary JSON value. + * + * It is immutable and assignable to any [JsonField], regardless of its expected type (i.e. its + * generic type argument). + */ @JsonDeserialize(using = JsonValue.Deserializer::class) sealed class JsonValue : JsonField() { @@ -204,6 +274,7 @@ sealed class JsonValue : JsonField() { fun convert(type: Class): R? = JSON_MAPPER.convertValue(this, type) + /** Returns the result of calling the [visitor] method corresponding to this value's variant. */ fun accept(visitor: Visitor): R = when (this) { is JsonMissing -> visitor.visitMissing() @@ -215,7 +286,12 @@ sealed class JsonValue : JsonField() { is JsonObject -> visitor.visitObject(values) } + /** + * An interface that defines how to map each variant state of a [JsonValue] to a value of type + * [R]. + */ interface Visitor { + fun visitNull(): R = visitDefault() fun visitMissing(): R = visitDefault() @@ -230,15 +306,52 @@ sealed class JsonValue : JsonField() { fun visitObject(values: Map): R = visitDefault() - fun visitDefault(): R { - throw RuntimeException("Unexpected value") - } + /** + * The default implementation for unimplemented visitor methods. + * + * @throws IllegalArgumentException in the default implementation. + */ + fun visitDefault(): R = throw IllegalArgumentException("Unexpected value") } companion object { private val JSON_MAPPER = jsonMapper() + /** + * Converts the given [value] to a [JsonValue]. + * + * This method works best on primitive types, [List] values, [Map] values, and nested + * combinations of these. For example: + * ```java + * // Create primitive JSON values + * JsonValue nullValue = JsonValue.from(null); + * JsonValue booleanValue = JsonValue.from(true); + * JsonValue numberValue = JsonValue.from(42); + * JsonValue stringValue = JsonValue.from("Hello World!"); + * + * // Create a JSON array value equivalent to `["Hello", "World"]` + * JsonValue arrayValue = JsonValue.from(List.of("Hello", "World")); + * + * // Create a JSON object value equivalent to `{ "a": 1, "b": 2 }` + * JsonValue objectValue = JsonValue.from(Map.of( + * "a", 1, + * "b", 2 + * )); + * + * // Create an arbitrarily nested JSON equivalent to: + * // { + * // "a": [1, 2], + * // "b": [3, 4] + * // } + * JsonValue complexValue = JsonValue.from(Map.of( + * "a", List.of(1, 2), + * "b", List.of(3, 4) + * )); + * ``` + * + * @throws IllegalArgumentException if [value] is not JSON serializable. + */ @JvmStatic fun from(value: Any?): JsonValue = when (value) { @@ -247,6 +360,11 @@ sealed class JsonValue : JsonField() { else -> JSON_MAPPER.convertValue(value, JsonValue::class.java) } + /** + * Returns a [JsonValue] converted from the given Jackson [JsonNode]. + * + * @throws IllegalStateException for unsupported node types. + */ @JvmStatic fun fromJsonNode(node: JsonNode): JsonValue = when (node.nodeType) { @@ -268,12 +386,19 @@ sealed class JsonValue : JsonField() { } class Deserializer : BaseDeserializer(JsonValue::class) { + override fun ObjectCodec.deserialize(node: JsonNode): JsonValue = fromJsonNode(node) override fun getNullValue(context: DeserializationContext?): JsonValue = JsonNull.of() } } +/** + * A class representing a "known" JSON serializable value of type [T], matching the type the SDK + * expects. + * + * It is assignable to `JsonField`. + */ class KnownValue private constructor( @com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: T @@ -292,44 +417,59 @@ private constructor( override fun toString() = value.contentToString() companion object { + + /** Returns a [KnownValue] containing the given [value]. */ @JsonCreator @JvmStatic fun of(value: T) = KnownValue(value) } } +/** + * A [JsonValue] representing an omitted JSON field. + * + * An instance of this class will cause a JSON field to be omitted from the serialized JSON + * entirely. + */ @JsonSerialize(using = JsonMissing.Serializer::class) class JsonMissing : JsonValue() { override fun toString() = "" companion object { + private val INSTANCE: JsonMissing = JsonMissing() + /** Returns the singleton instance of [JsonMissing]. */ @JvmStatic fun of() = INSTANCE } class Serializer : BaseSerializer(JsonMissing::class) { + override fun serialize( value: JsonMissing, generator: JsonGenerator, provider: SerializerProvider, ) { - throw RuntimeException("JsonMissing cannot be serialized") + throw IllegalStateException("JsonMissing cannot be serialized") } } } +/** A [JsonValue] representing a JSON `null` value. */ @JsonSerialize(using = NullSerializer::class) class JsonNull : JsonValue() { override fun toString() = "null" companion object { + private val INSTANCE: JsonNull = JsonNull() + /** Returns the singleton instance of [JsonMissing]. */ @JsonCreator @JvmStatic fun of() = INSTANCE } } +/** A [JsonValue] representing a JSON boolean value. */ class JsonBoolean private constructor( @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Boolean @@ -348,14 +488,18 @@ private constructor( override fun toString() = value.toString() companion object { + + /** Returns a [JsonBoolean] containing the given [value]. */ @JsonCreator @JvmStatic fun of(value: Boolean) = JsonBoolean(value) } } +/** A [JsonValue] representing a JSON number value. */ class JsonNumber private constructor( @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Number ) : JsonValue() { + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -369,10 +513,13 @@ private constructor( override fun toString() = value.toString() companion object { + + /** Returns a [JsonNumber] containing the given [value]. */ @JsonCreator @JvmStatic fun of(value: Number) = JsonNumber(value) } } +/** A [JsonValue] representing a JSON string value. */ class JsonString private constructor( @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: String @@ -391,10 +538,13 @@ private constructor( override fun toString() = value companion object { + + /** Returns a [JsonString] containing the given [value]. */ @JsonCreator @JvmStatic fun of(value: String) = JsonString(value) } } +/** A [JsonValue] representing a JSON array value. */ class JsonArray private constructor( @get:com.fasterxml.jackson.annotation.JsonValue @@ -415,10 +565,13 @@ private constructor( override fun toString() = values.toString() companion object { + + /** Returns a [JsonArray] containing the given [values]. */ @JsonCreator @JvmStatic fun of(values: List) = JsonArray(values.toImmutable()) } } +/** A [JsonValue] representing a JSON object value. */ class JsonObject private constructor( @get:com.fasterxml.jackson.annotation.JsonValue @@ -439,32 +592,62 @@ private constructor( override fun toString() = values.toString() companion object { + + /** Returns a [JsonObject] containing the given [values]. */ @JsonCreator @JvmStatic fun of(values: Map) = JsonObject(values.toImmutable()) } } +/** A Jackson annotation for excluding fields set to [JsonMissing] from the serialized JSON. */ @JacksonAnnotationsInside @JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class) annotation class ExcludeMissing +/** A class representing a field in a `multipart/form-data` request. */ class MultipartField private constructor( + /** A [JsonField] value, which will be serialized to zero or more parts. */ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField, + /** A content type for the serialized parts. */ @get:JvmName("contentType") val contentType: String, private val filename: String?, ) { companion object { + /** + * Returns a [MultipartField] containing the given [value] as a [KnownValue]. + * + * [contentType] will be set to `application/octet-stream` if [value] is binary data, or + * `text/plain; charset=utf-8` otherwise. + */ @JvmStatic fun of(value: T?) = builder().value(value).build() + /** + * Returns a [MultipartField] containing the given [value]. + * + * [contentType] will be set to `application/octet-stream` if [value] is binary data, or + * `text/plain; charset=utf-8` otherwise. + */ @JvmStatic fun of(value: JsonField) = builder().value(value).build() + /** + * Returns a mutable builder for constructing an instance of [MultipartField]. + * + * The following fields are required: + * ```java + * .value() + * ``` + * + * If [contentType] is unset, then it will be set to `application/octet-stream` if [value] + * is binary data, or `text/plain; charset=utf-8` otherwise. + */ @JvmStatic fun builder() = Builder() } + /** Returns the filename directive that will be included in the serialized field. */ fun filename(): Optional = Optional.ofNullable(filename) @JvmSynthetic @@ -486,8 +669,24 @@ private constructor( fun filename(filename: String?) = apply { this.filename = filename } + /** Alias for calling [Builder.filename] with `filename.orElse(null)`. */ fun filename(filename: Optional) = filename(filename.orElse(null)) + /** + * Returns an immutable instance of [MultipartField]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .value() + * ``` + * + * If [contentType] is unset, then it will be set to `application/octet-stream` if [value] + * is binary data, or `text/plain; charset=utf-8` otherwise. + * + * @throws IllegalStateException if any required field is unset. + */ fun build(): MultipartField { val value = checkRequired("value", value) return MultipartField(