Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom property support to AppConfig. #381

Merged
merged 1 commit into from
Apr 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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<AppConfigKey<*>, 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 <T : Any> get(key: AppConfigKey<T>): Maybe<T> {
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 {

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
nanodeath marked this conversation as resolved.
Show resolved Hide resolved
*
* @see AppConfigBuilder.withProperty
* @see AppConfig.getProperty
*/
interface AppConfigKey<T>
Original file line number Diff line number Diff line change
Expand Up @@ -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
nanodeath marked this conversation as resolved.
Show resolved Hide resolved
* 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 <T : Any> withProperty(key: AppConfigKey<T>, 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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.hexworks.cobalt.test

import org.assertj.core.api.AbstractObjectAssert
import org.hexworks.cobalt.datatypes.Maybe

fun <T> assertThat(maybe: Maybe<T>) = MaybeAssert(maybe)

class MaybeAssert<T>(actual: Maybe<T>) : AbstractObjectAssert<MaybeAssert<T>, Maybe<T>>(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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String>
private object TestAppConfigKey2 : AppConfigKey<String>

class AppConfigTest {

@Test
Expand All @@ -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<Int> {} // 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
Expand Down