import com.fasterxml.jackson.annotation.JsonValue import java.util.Optional import kotlin.reflect.KProperty /** * [Dynamic] represents the root node of any JSON document that could be of any * type that is representable in JSON. We use this instead of defining our own * models to map to. This ensures that we do not have to keep the models in sync * with the upstream applications, it also ensures that newly added data in an * upstream application is directly available as part of our application since * we are not mapping anything and thus not removing anything. * * @param data to wrap, never `null` because Jackson simply uses the native * `null` instead of trying to instantiate the class and we do the same since * Kotlin has great null handling. */ @Suppress("TooManyFunctions", "NOTHING_TO_INLINE", "UNCHECKED_CAST") class Dynamic(@JsonValue @JvmField val data: Any) { override fun equals(other: Any?) = other is Dynamic && data == other.data override fun hashCode() = data.hashCode() override fun toString() = "Dynamic($data)" /** * Treat this [Dynamic] as [List] and get the value at the given [index]. * * @see getNullable * @see maybeGet * @see maybeGetNullable * @throws DynamicTypeException if this [Dynamic] is no [List]. * @throws NoSuchDynamicElementException if no element exists at [index]. * @throws DynamicNullPointerException if the element at [index] is null. */ operator fun get(index: Int): Dynamic = asList().thenTry { get(index) }.toDynamic { "Element $index is null, use getNullable() if null is a valid value." } /** * Treat this [Dynamic] as [List] and get the value at the given [index]. * * @return null if the value at the given [index] is null. * @see get * @see maybeGet * @see maybeGetNullable * @throws DynamicTypeException if this [Dynamic] is no [List]. * @throws NoSuchDynamicElementException if no element exists at [index]. */ fun getNullable(index: Int): Dynamic? = asList().thenTry { get(index) }?.let(::Dynamic) /** * Treat this [Dynamic] as [List] and get the value at the given [index]. * * @return null if no [index] exists in the [List]. * @see get * @see getNullable * @see maybeGetNullable * @throws DynamicTypeException if this [Dynamic] is no [List]. * @throws DynamicNullPointerException if the value at [index] is null. */ fun maybeGet(index: Int): Dynamic? = asList().let { if (index >= it.size) return null it[index].toDynamic { "Element $index is null, use maybeGetNullable() if null is a valid value." } } /** * Treat this [Dynamic] as [List] and get the value at the given [index]. * * @return null if no [index] exists in the [List], [Optional.empty] if the * value at [index] is null, and otherwise an [Optional] with a [Dynamic] * wrapping the value. * @see get * @see getNullable * @see maybeGet * @throws DynamicTypeException if this [Dynamic] is no [List]. */ fun maybeGetNullable(index: Int): Optional? = asList().let { if (index >= it.size) return null Optional.ofNullable(it[index]?.let(::Dynamic)) } /** * Treat this [Dynamic] as [Map] and get the value of the given [property]. * * @see getNullable * @see maybeGet * @throws DynamicTypeException if this [Dynamic] is no [Map]. * @throws NoSuchDynamicPropertyException if [property] does not exist. * @throws DynamicNullPointerException if the [property]’s value is null. */ operator fun get(property: String): Dynamic = asMap().thenTry { getValue(property) }.toDynamic { "Property $property is null, use getNullable() if null is a valid value." } /** * Treat this [Dynamic] as [Map] and get the value of the given [property]. * * @return null if the value of the [property] is null. * @see get * @see maybeGet * @throws DynamicTypeException if this [Dynamic] is no [Map]. * @throws NoSuchDynamicPropertyException if [property] does not exist. */ fun getNullable(property: String): Dynamic? = asMap().thenTry { getValue(property) }?.let(::Dynamic) /** * Treat this [Dynamic] as [Map] and get the value of the given [property]. * * @return null if no [property] exists in the [Map]. * @see get * @see getNullable * @see maybeGetNullable * @throws DynamicTypeException if this [Dynamic] is no [Map]. * @throws DynamicNullPointerException if the [property]’s value is null. */ fun maybeGet(property: String): Dynamic? = asMap().let { if (!it.contains(property)) return null it.getValue(property).toDynamic { "Property $property is null, use maybeGetNullable() if null is a valid value." } } /** * Treat this [Dynamic] as [Map] and get the value of the given [property]. * * @return null if no [property] exists in the [Map], [Optional.empty] if * the value of the [property] is null, and otherwise an [Optional] with a * [Dynamic] wrapping the value. * @see get * @see getNullable * @see maybeGetNullable * @throws DynamicTypeException if this [Dynamic] is no [Map]. */ fun maybeGetNullable(property: String): Optional? = asMap().let { if (!it.contains(property)) return null Optional.ofNullable(it.getValue(property)?.let(::Dynamic)) } /** * Treat this [Dynamic] as [Map] and get the value of the given [property]. * * @param T the expected type of the value of the given [property], this * also decides whether the value is nullable, or not. * @throws DynamicTypeException if this [Dynamic] is no [Map] or the * [property]’s value is not of type [T]. * @throws NoSuchDynamicPropertyException if [property] does not exist. */ @JvmSynthetic operator fun getValue(receiver: Any?, property: KProperty<*>): T = asMap().thenTry { getValue(property.name) as T } /** * Get this [Dynamic]’s [data] as [List]. * * @throws DynamicTypeException if [data] is no [List]. */ fun asList() = unwrap>() /** * Get this [Dynamic]’s [data] as [Map]. * * @throws DynamicTypeException if [data] is no [Map]. */ fun asMap() = unwrap>() /** * Get the [data] of this [Dynamic] as [T]. * * @throws DynamicTypeException if [data] is not assignable to [T]. */ fun unwrap(type: Class): T = when { type.isAssignableFrom(data::class.java) -> data as T data is Double && type.isInstance(0F) -> data.toFloat() as T else -> throw DynamicTypeException("Cannot assign ${data::class.simpleName} to ${type.simpleName}.") } /** * Get the [data] of this [Dynamic] as [T]. * * @throws DynamicTypeException if [data] is not assignable to [T]. */ @JvmSynthetic inline fun unwrap(): T = unwrap(T::class.java) /** * Treat this [Dynamic] as [List] and wrap each element in a [Dynamic]. * * @see wrapSparseList * @throws DynamicTypeException if this [Dynamic] is no [List]. * @throws DynamicNullPointerException if any element is null. */ fun wrapList(): List = asList().mapIndexed { i, it -> it.toDynamic { "Element $i was null, use wrapSparseList() if null is a valid value." } } /** * Treat this [Dynamic] as [List] and wrap each element in a [Dynamic] * unless it is null. * * @see wrapList * @throws DynamicTypeException if this [Dynamic] is no [List]. */ fun wrapSparseList(): List = asList().map { it?.let(::Dynamic) } /** * Treat this [Dynamic] as [Map] and wrap each value in a [Dynamic]. * * @see wrapSparseMap * @throws DynamicTypeException if this [Dynamic] is no [Map]. * @throws DynamicNullPointerException if any value is null. */ fun wrapMap(): Map = asMap().mapValues { (property, value) -> value.toDynamic { "Property $property was null, use wrapSparseMap() if null is a valid value." } } /** * Treat this [Dynamic] as [Map] and wrap each value in a [Dynamic] unless * it is null. * * @see wrapMap * @throws DynamicTypeException if this [Dynamic] is no [Map]. */ fun wrapSparseMap(): Map = asMap().mapValues { it.value?.let(::Dynamic) } @Suppress("ThrowsCount", "TooGenericExceptionCaught") private inline fun T.thenTry(block: T.() -> R): R = try { block() } catch (cause: ClassCastException) { throw DynamicTypeException(cause.message, cause) } catch (cause: IndexOutOfBoundsException) { throw NoSuchDynamicElementException(cause.message, cause) } catch (cause: NullPointerException) { throw DynamicNullPointerException(cause.message, cause) } catch (cause: NoSuchElementException) { throw NoSuchDynamicPropertyException(cause.message, cause) } private inline fun Any?.toDynamic(message: () -> String): Dynamic = when (this) { null -> throw DynamicNullPointerException(message()) else -> Dynamic(this) } /** Marker interface for all dynamic exceptions. */ interface DynamicException /** A value that is not allowed to be null was null. */ class DynamicNullPointerException( override val message: String?, override val cause: Throwable? = null ) : NullPointerException(), DynamicException /** A value that was expected to be of a certain type was not. */ class DynamicTypeException( override val message: String?, override val cause: Throwable? = null ) : ClassCastException(), DynamicException /** A property that was expected to be present was not. */ class NoSuchDynamicPropertyException( override val message: String?, override val cause: Throwable? = null ) : NoSuchElementException(), DynamicException /** An element at an index that was expected to be present was not. */ class NoSuchDynamicElementException( override val message: String?, override val cause: Throwable? = null ) : IndexOutOfBoundsException(), DynamicException }