Something went wrong. Try again.
Share your Ktor endpoints between client and server spine.opensavvy.dev
rest http ktor kotlin arrow
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378package opensavvy.spine.api
import kotlin.jvm.JvmInlineimport kotlin.reflect.KPropertyimport kotlin.uuid.Uuid
/** * Underlying storage for [Parameters]: a mutable map of parameter names to their raw string values. * * Library integrations may use this directly when constructing parameter bundles. */typealias ParameterStorage = MutableMap<String, List<String>>
/** * Factory function that builds a [Parameters] subtype backed by a [ParameterStorage]. * * This type appears when the client calls an endpoint with parameters. * To learn more, see [Parameters]. */typealias ParameterConstructor<P> = (ParameterStorage) -> P
/** * Additional parameters for [endpoints][AnyEndpoint]. * * Endpoints declare mandatory parameters, such as the identifier of the resource. * This abstract class allows to declare additional parameters in a type-safe manner. * * ### Usage * * In common code, declare a class that inherits from [Parameters], and use it to declare the name and type of the available parameters: * ```kotlin * class MyRequestParameters(data: ParameterStorage) : Parameters(data) { * var param1: String? by parameter() * var isSubscribed: Boolean by parameter("is_subscribed", default = false) * } * ``` * * To create a new parameter bundle, use the helper [buildParameters] function: * ```kotlin * val example = buildParameters(::MyRequestParameters) { * param1 = "value" * isSubscribed = true * } * ``` * * The values can be accessed in a type-safe manner: * ```kotlin * println(example.param1) * ``` * The values can also be accessed via their string representation: * ```kotlin * println(example.data["param1"]) * ``` */abstract class Parameters( /** * Internal string representation of the parameters. */ val data: ParameterStorage = HashMap(),) {
/** * Declares an optional parameter of type [T]. * * If the parameter is missing, reading it will return [default] instead. * * The parameter is automatically named after the variable it is assigned to. * * ### Example * * ```kotlin * class ListUsers(data: ParameterStorage) : Parameters(data) { * var includeArchived by parameter(default = false) * } * ``` */ protected fun <T : Any> parameter(default: T) = UnnamedParameter(default)
/** * Declares an optional parameter [name] of type [T]. * * If the parameter is missing, reading it will return [default] instead. * * ### Example * * ```kotlin * class ListUsers(data: ParameterStorage) : Parameters(data) { * var includeArchived by parameter("include_archived", default = false) * } * ``` */ protected fun <T : Any> parameter(name: String, default: T) = Parameter(name, default)
/** * Declares a parameter of type [T]. * * If [T] is nullable, this function declares an optional parameter with a default value of `null`. * If [T] is non-nullable, this function declares a mandatory parameter which will throw a [NoSuchElementException] if no value is provided. * * The parameter is automatically named after the variable it is assigned to. * * ### Example * * ```kotlin * class ListUsers(data: ParameterStorage) : Parameters(data) { * var includeArchived: String? by parameter() * } * ``` */ protected fun <T> parameter() = UnnamedParameter<T>(null)
/** * Declares a parameter [name] of type [T]. * * If [T] is nullable, this function declares an optional parameter with a default value of `null`. * If [T] is non-nullable, this function declares a mandatory parameter which will throw a [NoSuchElementException] if no value is provided. * * ### Example * * ```kotlin * class ListUsers(data: ParameterStorage) : Parameters(data) { * var includeArchived: String? by parameter("include_archived") * } * ``` */ protected fun <T> parameter(name: String) = Parameter<T>(name, null)
/** * Declares a list of parameters of type [T]. * * If the parameter is missing, reading it will return [emptyList] instead. * * The parameter is automatically named after the variable it is assigned to. * * ### Example * * ```kotlin * class ListUsers(data: ParameterStorage) : Parameters(data) { * var tags by listParameter<String>() * } * ``` */ protected fun <T> listParameter() = UnnamedListParameter<T>()
/** * Declares a list of parameters [name] of type [T]. * * If the parameter is missing, reading it will return [emptyList] instead. * * ### Example * * ```kotlin * class ListUsers(data: ParameterStorage) : Parameters(data) { * var tags by listParameter<String>("search_tags") * } * ``` */ protected fun <T> listParameter(name: String) = ListParameter<T>(name)
override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false
other as Parameters
return data == other.data }
override fun hashCode(): Int { return data.hashCode() }
override fun toString(): String = "${this::class.toString().removePrefix("class ")}$data"
/** * Internal type used by the parameter declaration syntax. * * See [Parameters]. */ @JvmInline value class UnnamedParameter<T> internal constructor(val defaultValue: T?)
/** * A declared query parameter in an API schema. * * See [Parameters]. */ class Parameter<T>( /** * Name of the parameter as it appears in the URL. */ val name: String,
/** * The value that is substituted in if the URL does not contain this parameter. */ val defaultValue: T?, ) {
init { require(name.isNotBlank()) { "The name of a parameter cannot be empty: '$name'" } require(name.none { it.isWhitespace() }) { "The name of a parameter cannot contain whitespace: '$name'" } } }
/** * Internal type used by the parameter declaration syntax. * * See [Parameters]. */ @Suppress("unused") // The parameter T is unused for now but may be used in the future class UnnamedListParameter<T>
/** * A declared query list parameter in an API schema. * * See [Parameters]. */ @Suppress("unused") // The parameter T is unused for now but may be used in the future class ListParameter<T>( /** * Name of the parameter as it appears in the URL. */ val name: String, ) { init { require(name.isNotBlank()) { "The name of a parameter cannot be empty: '$name'" } require(name.none { it.isWhitespace() }) { "The name of a parameter cannot contain whitespace: '$name'" } } }
/** * The default parameter instance. * * Use this instance for operations that take no parameters. */ object Empty : Parameters()}
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */inline operator fun <T> Parameters.UnnamedParameter<T>.provideDelegate(thisRef: Parameters, property: KProperty<*>) = Parameters.Parameter(property.name, this.defaultValue)
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */inline operator fun <T> Parameters.UnnamedListParameter<T>.provideDelegate(thisRef: Parameters, property: KProperty<*>) = Parameters.ListParameter<T>(property.name)
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */inline operator fun <reified T> Parameters.Parameter<T>.getValue(thisRef: Parameters, property: KProperty<*>): T { val value = thisRef.data[name]?.let { if (it.size == 1) it[0] else throw IllegalArgumentException( """ The parameter '$name' should not contain multiple values. If you want to accept multiple parameters, use listParameters() instead of parameters(). """.trimIndent() ) } ?: return run { if (defaultValue is T) defaultValue else throw NoSuchElementException("The parameter '$name' is mandatory, but no value was provided.") }
return stringToValue(value)}
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.listParameter]. */inline operator fun <reified T> Parameters.ListParameter<T>.getValue(thisRef: Parameters, property: KProperty<*>): List<T> { val value = thisRef.data[name] ?: return emptyList()
return value.map { stringToValue<T>(it) }}
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */@PublishedApiinternal inline fun <reified T> stringToValue(value: String) = when (T::class) { String::class -> value as T Boolean::class -> value.toBooleanStrict() as T Byte::class -> value.toByte() as T Short::class -> value.toShort() as T Int::class -> value.toInt() as T Long::class -> value.toLong() as T UByte::class -> value.toUByte() as T UShort::class -> value.toUShort() as T UInt::class -> value.toUInt() as T ULong::class -> value.toULong() as T Float::class -> value.toFloat() as T Double::class -> value.toDouble() as T Uuid::class -> Uuid.parse(value) as T else -> throw UnsupportedOperationException("The type ${T::class.simpleName ?: T::class.toString()} is not currently supported in parameters.")}
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */inline operator fun <reified T> Parameters.Parameter<T>.setValue(thisRef: Parameters, property: KProperty<*>, value: T) { if (value == null) thisRef.data -= name else thisRef.data[name] = listOf(valueToString(value))}
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.listParameter]. */inline operator fun <reified T> Parameters.ListParameter<T>.setValue(thisRef: Parameters, property: KProperty<*>, value: List<T>) { if (value.isEmpty()) thisRef.data -= name else thisRef.data[name] = value.map(::valueToString)}
/** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */@PublishedApiinternal inline fun <reified T> valueToString(value: T) = when (value) { is String -> value is Boolean -> value.toString() is Byte -> value.toString() is Short -> value.toString() is Int -> value.toString() is Long -> value.toString() is UByte -> value.toString() is UShort -> value.toString() is UInt -> value.toString() is ULong -> value.toString() is Float -> value.toString() is Double -> value.toString() is Uuid -> value.toString() else -> throw UnsupportedOperationException("The type ${T::class.simpleName ?: T::class.toString()} is not currently supported in parameters.")}
/** * Creates and configures a [Parameters] instance. * * ### Example * * ```kotlin * class Example(data: ParameterStorage) : Parameters(data) { * var includeArchived by parameter(default = false) * } * * val example = buildParameters(::Example) { * includeArchived = true * } * ``` */fun <P : Parameters> buildParameters(construct: ParameterConstructor<P>, block: P.() -> Unit): P { return construct(HashMap()).apply(block)}