@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST") package app.revanced.patcher.patch import java.io.File import java.nio.file.Path import kotlin.reflect.KProperty class NoSuchOptionException(val option: String) : Exception("No such option: $option") class IllegalValueException(val value: Any?) : Exception("Illegal value: $value") class InvalidTypeException(val got: String, val expected: String) : Exception("Invalid option value type: $got, expected $expected") object RequirementNotMetException : Exception("null was passed into an option that requires a value") /** * A registry for an array of [PatchOption]s. * @param options An array of [PatchOption]s. */ class PatchOptions(vararg options: PatchOption<*>) : Iterable> { private val register = mutableMapOf>() init { options.forEach { register(it) } } internal fun register(option: PatchOption<*>) { if (register.containsKey(option.key)) { throw IllegalStateException("Multiple options found with the same key") } register[option.key] = option } /** * Get a [PatchOption] by its key. * @param key The key of the [PatchOption]. */ operator fun get(key: String) = register[key] ?: throw NoSuchOptionException(key) /** * Set the value of a [PatchOption]. * @param key The key of the [PatchOption]. * @param value The value you want it to be. * Please note that using the wrong value type results in a runtime error. */ inline operator fun set(key: String, value: T) { @Suppress("UNCHECKED_CAST") val opt = get(key) as PatchOption if (opt.value !is T) throw InvalidTypeException( T::class.java.canonicalName, opt.value?.let { it::class.java.canonicalName } ?: "null" ) opt.value = value } /** * Sets the value of a [PatchOption] to `null`. * @param key The key of the [PatchOption]. */ fun nullify(key: String) { get(key).value = null } override fun iterator() = register.values.iterator() } /** * A [Patch] option. * @param key Unique identifier of the option. Example: _`settings.microg.enabled`_ * @param default The default value of the option. * @param title A human-readable title of the option. Example: _MicroG Settings_ * @param description A human-readable description of the option. Example: _Settings integration for MicroG._ * @param required Whether the option is required. */ @Suppress("MemberVisibilityCanBePrivate") sealed class PatchOption( val key: String, default: T?, val title: String, val description: String, val required: Boolean, val validator: (T?) -> Boolean ) { var value: T? = default get() { if (field == null && required) { throw RequirementNotMetException } return field } set(value) { if (value == null && required) { throw RequirementNotMetException } if (!validator(value)) { throw IllegalValueException(value) } field = value } /** * Gets the value of the option. * Please note that using the wrong value type results in a runtime error. */ inline operator fun getValue(thisRef: Any?, property: KProperty<*>): V? { if (value !is V?) throw InvalidTypeException( V::class.java.canonicalName, value?.let { it::class.java.canonicalName } ?: "null" ) return value as? V? } /** * Gets the value of the option. * Please note that using the wrong value type results in a runtime error. */ inline operator fun setValue(thisRef: Any?, property: KProperty<*>, new: V) { if (value !is V) throw InvalidTypeException( V::class.java.canonicalName, value?.let { it::class.java.canonicalName } ?: "null" ) value = new as T } /** * A [PatchOption] representing a [String]. * @see PatchOption */ class StringOption( key: String, default: String?, title: String, description: String, required: Boolean = false, validator: (String?) -> Boolean = { true } ) : PatchOption( key, default, title, description, required, validator ) /** * A [PatchOption] representing a [Boolean]. * @see PatchOption */ class BooleanOption( key: String, default: Boolean?, title: String, description: String, required: Boolean = false, validator: (Boolean?) -> Boolean = { true } ) : PatchOption( key, default, title, description, required, validator ) /** * A [PatchOption] with a list of allowed options. * @param options A list of allowed options for the [ListOption]. * @see PatchOption */ sealed class ListOption( key: String, default: E?, val options: Iterable, title: String, description: String, required: Boolean = false, validator: (E?) -> Boolean = { true } ) : PatchOption( key, default, title, description, required, { (it?.let { it in options } ?: true) && validator(it) } ) { init { if (default != null && default !in options) { throw IllegalStateException("Default option must be an allowed option") } } } /** * A [ListOption] of type [String]. * @see ListOption */ class StringListOption( key: String, default: String?, options: Iterable, title: String, description: String, required: Boolean = false, validator: (String?) -> Boolean = { true } ) : ListOption( key, default, options, title, description, required, validator ) /** * A [ListOption] of type [Int]. * @see ListOption */ class IntListOption( key: String, default: Int?, options: Iterable, title: String, description: String, required: Boolean = false, validator: (Int?) -> Boolean = { true } ) : ListOption( key, default, options, title, description, required, validator ) /** * A [PatchOption] representing a [Path]. * @see PatchOption */ open class PathOption( key: String, default: Path?, title: String, description: String, required: Boolean = false, validator: (Path?) -> Boolean = { true } ) : PatchOption( key, default, title, description, required, validator ) /** * A [PathOption] of type [File]. * @see PathOption */ class FileOption( key: String, default: File?, title: String, description: String, required: Boolean = false, validator: (File?) -> Boolean = { true } ) : PathOption( key, default?.toPath(), title, description, required, { validator(it?.toFile()) } ) }