diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfig.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfig.kt index ae94ca4d60..2a99743779 100644 --- a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfig.kt +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfig.kt @@ -1,5 +1,6 @@ package org.hexworks.zircon.api.application +import org.hexworks.cobalt.datatypes.Maybe import org.hexworks.zircon.api.CP437TilesetResources import org.hexworks.zircon.api.ColorThemes import org.hexworks.zircon.api.GraphicalTilesetResources @@ -14,6 +15,8 @@ import kotlin.jvm.JvmStatic * Object that encapsulates the configuration parameters for an [Application]. * This includes properties such as the shape of the cursor, the color of the cursor * and if the cursor should blink or not. + * + * Typically you'll want to construct this using [AppConfigBuilder], not AppConfig's constructor. */ @Suppress("ArrayInDataClass") data class AppConfig( @@ -114,14 +117,33 @@ data class AppConfig( * If set [iconPath] will contain the path of the resource that points * to an icon image that will be used in the application window. */ - val iconPath: String? = null + val iconPath: String? = null, + /** + * If set, contains custom properties that plugin authors can set and access. + */ + internal val customProperties: Map, Any> = emptyMap() ) { /** * Tells whether bounds check should be performed or not. * This depends on the various debug mode configurations. */ - fun shouldCheckBounds() = debugMode.not() || (debugMode && debugConfig.relaxBoundsCheck.not()) + fun shouldCheckBounds() = !debugMode || !debugConfig.relaxBoundsCheck + + /** + * Retrieve a custom property set earlier using [AppConfigBuilder.withProperty]. If this property was + * never set, returns an empty [Maybe]. + * + * ### End Developers + * + * You probably don't need to call this API. + */ + operator fun get(key: AppConfigKey): Maybe { + val value: Any? = customProperties[key] + // This is actually a safe cast because of the way `withProperty` is defined. + @Suppress("UNCHECKED_CAST") + return Maybe.ofNullable(value as T?) + } companion object { diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfigKey.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfigKey.kt new file mode 100644 index 0000000000..01814fa19d --- /dev/null +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/application/AppConfigKey.kt @@ -0,0 +1,12 @@ +package org.hexworks.zircon.api.application + +import org.hexworks.zircon.api.builder.application.AppConfigBuilder + +/** + * This simple interface is used to set and retrieve custom properties on [AppConfig] + * in a typesafe way. + * + * @see AppConfigBuilder.withProperty + * @see AppConfig.getProperty + */ +interface AppConfigKey diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/application/AppConfigBuilder.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/application/AppConfigBuilder.kt index 7af9b79791..dc86f5f819 100644 --- a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/application/AppConfigBuilder.kt +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/application/AppConfigBuilder.kt @@ -191,6 +191,27 @@ data class AppConfigBuilder( ) } + /** + * Adds a custom property into the AppConfig object. This can later be retrieved using [AppConfig.getProperty]. + * + * ### End Developers + * + * You probably don't need to call this API. + * + * ### Plugin Developers + * + * Write extension methods off of [AppConfigBuilder] that call this API in order to enable end developers + * to pass configuration in through AppConfig that your plugin can later use. It's recommended that [key] + * be an `object` with minimal visibility (e.g. `internal`). + * + * @sample org.hexworks.zircon.api.application.AppConfigTest.propertyExample + */ + fun withProperty(key: AppConfigKey, value: T): AppConfigBuilder = also { + config = config.copy( + customProperties = config.customProperties + (key to value) + ) + } + @Deprecated("This will be removed in the next version, as the behavior is inconsistent.") fun withFullScreen(screenWidth: Int, screenHeight: Int) = also { throw UnsupportedOperationException("Unstable api, use withFullScreen(true) instead") diff --git a/zircon.core/src/jvmTest/kotlin/org/hexworks/cobalt/test/MaybeAssert.kt b/zircon.core/src/jvmTest/kotlin/org/hexworks/cobalt/test/MaybeAssert.kt new file mode 100644 index 0000000000..3d1285b1a2 --- /dev/null +++ b/zircon.core/src/jvmTest/kotlin/org/hexworks/cobalt/test/MaybeAssert.kt @@ -0,0 +1,35 @@ +package org.hexworks.cobalt.test + +import org.assertj.core.api.AbstractObjectAssert +import org.hexworks.cobalt.datatypes.Maybe + +fun assertThat(maybe: Maybe) = MaybeAssert(maybe) + +class MaybeAssert(actual: Maybe) : AbstractObjectAssert, Maybe>(actual, MaybeAssert::class.java) { + fun isPresent() = also { + isNotNull + + if (!actual.isPresent) { + failWithMessage("Expected a value to be present but was empty") + } + } + + fun isEmpty() = also { + isNotNull + + if (!actual.isEmpty()) { + failWithMessage("Expected no value to be present but contained %s", actual.get()) + } + } + + fun hasValue(expected: T) = also { + isNotNull + + if (actual.isEmpty()) { + failWithMessage("Expected a value to be present but was empty") + } + if (actual.get() != expected) { + failWithMessage("Expected actual <%s> to be equal to <%s> but was not", actual.get(), expected) + } + } +} diff --git a/zircon.core/src/jvmTest/kotlin/org/hexworks/zircon/api/application/AppConfigTest.kt b/zircon.core/src/jvmTest/kotlin/org/hexworks/zircon/api/application/AppConfigTest.kt index dbb99e7d01..e04095b520 100644 --- a/zircon.core/src/jvmTest/kotlin/org/hexworks/zircon/api/application/AppConfigTest.kt +++ b/zircon.core/src/jvmTest/kotlin/org/hexworks/zircon/api/application/AppConfigTest.kt @@ -1,10 +1,14 @@ package org.hexworks.zircon.api.application import org.assertj.core.api.Assertions.assertThat +import org.hexworks.cobalt.test.assertThat import org.hexworks.zircon.api.builder.application.AppConfigBuilder import org.hexworks.zircon.api.color.ANSITileColor import org.junit.Test +private object TestAppConfigKey : AppConfigKey +private object TestAppConfigKey2 : AppConfigKey + class AppConfigTest { @Test @@ -29,6 +33,60 @@ class AppConfigTest { .isEqualTo(HAS_CLIPBOARD) } + @Test + fun propertyUnset() { + val appConfig = AppConfigBuilder.newBuilder().build() + assertThat(appConfig[TestAppConfigKey]) + .isEmpty() + } + + @Test + fun propertySet() { + val appConfig = AppConfigBuilder.newBuilder() + .withProperty(TestAppConfigKey, "foo") + .build() + assertThat(appConfig[TestAppConfigKey]) + .hasValue("foo") + } + + @Test + fun propertyOverwrite() { + val appConfig = AppConfigBuilder.newBuilder() + .withProperty(TestAppConfigKey, "foo") + .withProperty(TestAppConfigKey, "bar") + .build() + assertThat(appConfig[TestAppConfigKey]) + .hasValue("bar") + } + + @Test + fun propertyMultiple() { + val appConfig = AppConfigBuilder.newBuilder() + .withProperty(TestAppConfigKey, "foo") + .withProperty(TestAppConfigKey2, "bar") + .build() + assertThat(appConfig[TestAppConfigKey]) + .hasValue("foo") + assertThat(appConfig[TestAppConfigKey2]) + .hasValue("bar") + } + + @Test + fun propertyExample() { + // Plugin API + val key = object : AppConfigKey {} // use a real internal or private `object`, not an anonymous one! + fun AppConfigBuilder.enableCoolFeature() = also { withProperty(key, 42) } + + // User code + val appConfig = AppConfigBuilder.newBuilder() + .enableCoolFeature() + .build() + + // Plugin internals + assertThat(appConfig[key]) + .hasValue(42) + } + companion object { val BLINK_TIME = 5L val CURSOR_STYLE = CursorStyle.UNDER_BAR