package opensavvy.spine.api import kotlin.jvm.JvmInline import kotlin.reflect.KProperty import 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> /** * 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

= (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 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 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 parameter() = UnnamedParameter(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 parameter(name: String) = Parameter(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() * } * ``` */ protected fun listParameter() = UnnamedListParameter() /** * 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("search_tags") * } * ``` */ protected fun listParameter(name: String) = ListParameter(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 internal constructor(val defaultValue: T?) /** * A declared query parameter in an API schema. * * See [Parameters]. */ class Parameter( /** * 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 /** * 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( /** * 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 Parameters.UnnamedParameter.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 Parameters.UnnamedListParameter.provideDelegate(thisRef: Parameters, property: KProperty<*>) = Parameters.ListParameter(property.name) /** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */ inline operator fun Parameters.Parameter.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 Parameters.ListParameter.getValue(thisRef: Parameters, property: KProperty<*>): List { val value = thisRef.data[name] ?: return emptyList() return value.map { stringToValue(it) } } /** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */ @PublishedApi internal inline fun 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 Parameters.Parameter.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 Parameters.ListParameter.setValue(thisRef: Parameters, property: KProperty<*>, value: List) { if (value.isEmpty()) thisRef.data -= name else thisRef.data[name] = value.map(::valueToString) } /** * Internal method used by the parameter declaration syntax. * * See [Parameters.parameter]. */ @PublishedApi internal inline fun 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

buildParameters(construct: ParameterConstructor

, block: P.() -> Unit): P { return construct(HashMap()).apply(block) }