├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Libraries.kt │ ├── Publishing.kt │ └── Versions.kt ├── cobalt.core ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── org │ │ └── hexworks │ │ └── cobalt │ │ ├── core │ │ ├── api │ │ │ ├── UUID.kt │ │ │ ├── behavior │ │ │ │ ├── Disposable.kt │ │ │ │ └── DisposeState.kt │ │ │ └── extensions │ │ │ │ ├── Functions.kt │ │ │ │ ├── IdentifierExtensions.kt │ │ │ │ └── Predicates.kt │ │ └── internal │ │ │ ├── Atom.kt │ │ │ ├── DefaultUUID.kt │ │ │ └── impl │ │ │ └── DefaultAtom.kt │ │ ├── databinding │ │ ├── api │ │ │ ├── Cobalt.kt │ │ │ ├── binding │ │ │ │ ├── Binding.kt │ │ │ │ ├── BooleanBindings.kt │ │ │ │ ├── CollectionBindings.kt │ │ │ │ ├── DoubleBindings.kt │ │ │ │ ├── FloatBindings.kt │ │ │ │ ├── GenericBindings.kt │ │ │ │ ├── IntBindings.kt │ │ │ │ ├── LongBindings.kt │ │ │ │ └── StringBindings.kt │ │ │ ├── collection │ │ │ │ ├── CollectionProperty.kt │ │ │ │ ├── ListProperty.kt │ │ │ │ ├── MapProperty.kt │ │ │ │ ├── ObservableCollection.kt │ │ │ │ ├── ObservableList.kt │ │ │ │ ├── ObservableListBinding.kt │ │ │ │ ├── ObservableMap.kt │ │ │ │ ├── ObservableSet.kt │ │ │ │ ├── ObservableSetBinding.kt │ │ │ │ ├── SetProperty.kt │ │ │ │ ├── WritableCollection.kt │ │ │ │ ├── WritableList.kt │ │ │ │ ├── WritableMap.kt │ │ │ │ └── WritableSet.kt │ │ │ ├── converter │ │ │ │ ├── Converter.kt │ │ │ │ ├── Converters.kt │ │ │ │ ├── IdentityConverter.kt │ │ │ │ └── IsomorphicConverter.kt │ │ │ ├── event │ │ │ │ └── ObservableValueChanged.kt │ │ │ ├── extension │ │ │ │ ├── AnyExtensions.kt │ │ │ │ ├── ListExtensions.kt │ │ │ │ ├── Properties.kt │ │ │ │ ├── Subscriptions.kt │ │ │ │ └── TypeAliases.kt │ │ │ ├── property │ │ │ │ ├── Property.kt │ │ │ │ ├── PropertyDelegate.kt │ │ │ │ └── PropertyValidator.kt │ │ │ └── value │ │ │ │ ├── ObservableValue.kt │ │ │ │ ├── Value.kt │ │ │ │ ├── ValueValidationFailedException.kt │ │ │ │ ├── ValueValidationResult.kt │ │ │ │ └── WritableValue.kt │ │ └── internal │ │ │ ├── binding │ │ │ ├── BaseBinding.kt │ │ │ ├── BidirectionalBinding.kt │ │ │ ├── ComputedBinding.kt │ │ │ ├── ComputedDualBinding.kt │ │ │ ├── ListBinding.kt │ │ │ ├── SetBinding.kt │ │ │ └── UnidirectionalBinding.kt │ │ │ ├── collections │ │ │ ├── DefaultListProperty.kt │ │ │ ├── DefaultMapProperty.kt │ │ │ ├── DefaultPropertyListProperty.kt │ │ │ ├── DefaultPropertyMapProperty.kt │ │ │ ├── DefaultPropertySetProperty.kt │ │ │ ├── DefaultSetProperty.kt │ │ │ ├── ListBindingDecorator.kt │ │ │ └── SetBindingDecorator.kt │ │ │ ├── event │ │ │ └── PropertyScope.kt │ │ │ ├── exception │ │ │ └── CircularBindingException.kt │ │ │ ├── extensions │ │ │ ├── BindingExtensions.kt │ │ │ └── PropertyExtensions.kt │ │ │ └── property │ │ │ ├── DefaultProperty.kt │ │ │ ├── DefaultPropertyDelegate.kt │ │ │ ├── InternalProperty.kt │ │ │ └── base │ │ │ └── BaseProperty.kt │ │ ├── events │ │ ├── api │ │ │ ├── CallbackResult.kt │ │ │ ├── Event.kt │ │ │ ├── EventBus.kt │ │ │ ├── EventBusExtensions.kt │ │ │ ├── EventExtensions.kt │ │ │ ├── EventScope.kt │ │ │ └── Subscription.kt │ │ └── internal │ │ │ ├── ApplicationScope.kt │ │ │ └── DefaultEventBus.kt │ │ └── logging │ │ ├── api │ │ ├── LogException.kt │ │ ├── Logger.kt │ │ └── LoggerFactory.kt │ │ └── internal │ │ └── DefaultLogger.kt │ └── commonTest │ └── kotlin │ └── org │ └── hexworks │ └── cobalt │ ├── core │ └── internal │ │ └── AtomTest.kt │ ├── databinding │ └── internal │ │ ├── binding │ │ ├── BidirectionalConverterBindingTest.kt │ │ ├── CollectionBindingsTest.kt │ │ └── ComputedDualBindingTest.kt │ │ ├── collections │ │ ├── DefaultListPropertyTest.kt │ │ ├── DefaultMapPropertyTest.kt │ │ ├── DefaultPropertyListPropertyTest.kt │ │ ├── DefaultPropertySetPropertyTest.kt │ │ └── DefaultSetPropertyTest.kt │ │ ├── expression │ │ ├── BooleanExpressionsTest.kt │ │ ├── LongExpressionsTest.kt │ │ └── StringExpressionsTest.kt │ │ └── property │ │ ├── DefaultPropertyDelegateTest.kt │ │ └── DefaultPropertyTest.kt │ └── events │ └── EventBusTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── script └── release └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ignore all differences in line endings 2 | * -crlf -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: '17' 22 | distribution: 'temurin' 23 | 24 | # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies. 25 | # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md 26 | - name: Setup Gradle 27 | uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0 28 | 29 | - name: Build with Gradle Wrapper 30 | run: ./gradlew build 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | logs/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | out/ 6 | .idea 7 | *.iws 8 | *.iml 9 | *.ipr 10 | node_modules/ 11 | docs/ 12 | .DS_Store 13 | kotlin-js-store 14 | secring.gpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cobalt 2 | 3 | **Cobalt** is a *multiplatform* utilities library for Kotlin. It has *JVM* and *JS* targets 4 | and contains modules for *data binding*, *logging*, *eventing* and *networking*. **Note that** 5 | Cobalt is only intended to be used from **Kotlin** projects and it is not optimized for *JS* nor 6 | *Java* projects. 7 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | mavenLocal() 4 | mavenCentral() 5 | maven("https://dl.bintray.com/kotlin/kotlinx") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenLocal() 7 | mavenCentral() 8 | google() 9 | maven { url = uri("https://plugins.gradle.org/m2/") } 10 | } 11 | 12 | dependencies { 13 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") 14 | } 15 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Libraries.kt: -------------------------------------------------------------------------------- 1 | import Versions.KOR_VERSION 2 | import Versions.KOTLINX_COLLECTIONS_IMMUTABLE_VERSION 3 | 4 | object Libraries { 5 | 6 | const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect" 7 | const val KOTLINX_COLLECTIONS_IMMUTABLE = 8 | "org.jetbrains.kotlinx:kotlinx-collections-immutable:$KOTLINX_COLLECTIONS_IMMUTABLE_VERSION" 9 | 10 | const val KORGE_FOUNDATION = "com.soywiz.korge:korge-foundation:$KOR_VERSION" 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Publishing.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("LocalVariableName") 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.publish.PublishingExtension 5 | import org.gradle.api.publish.maven.MavenPublication 6 | import org.gradle.api.tasks.bundling.Jar 7 | import org.gradle.kotlin.dsl.getValue 8 | import org.gradle.kotlin.dsl.provideDelegate 9 | import org.gradle.kotlin.dsl.registering 10 | import org.gradle.kotlin.dsl.withType 11 | 12 | fun PublishingExtension.publishWith( 13 | project: Project, 14 | module: String, 15 | desc: String 16 | ) { 17 | 18 | with(project) { 19 | 20 | val emptyJavadocJar by tasks.registering(Jar::class) { 21 | archiveClassifier.set("javadoc") 22 | } 23 | 24 | val POM_URL: String by project 25 | val POM_SCM_URL: String by project 26 | val POM_SCM_CONNECTION: String by project 27 | val POM_SCM_DEV_CONNECTION: String by project 28 | val POM_LICENCE_NAME: String by project 29 | val POM_LICENCE_URL: String by project 30 | val POM_LICENCE_DIST: String by project 31 | val POM_DEVELOPER_ID: String by project 32 | val POM_DEVELOPER_NAME: String by project 33 | val POM_DEVELOPER_EMAIL: String by project 34 | val POM_DEVELOPER_ORGANIZATION: String by project 35 | val POM_DEVELOPER_ORGANIZATION_URL: String by project 36 | 37 | publications.withType().all { 38 | 39 | pom { 40 | 41 | name.set(module) 42 | description.set(desc) 43 | url.set(POM_URL) 44 | 45 | scm { 46 | url.set(POM_SCM_URL) 47 | connection.set(POM_SCM_CONNECTION) 48 | developerConnection.set(POM_SCM_DEV_CONNECTION) 49 | } 50 | 51 | licenses { 52 | license { 53 | name.set(POM_LICENCE_NAME) 54 | url.set(POM_LICENCE_URL) 55 | distribution.set(POM_LICENCE_DIST) 56 | } 57 | } 58 | 59 | developers { 60 | developer { 61 | id.set(POM_DEVELOPER_ID) 62 | name.set(POM_DEVELOPER_NAME) 63 | email.set(POM_DEVELOPER_EMAIL) 64 | organization.set(POM_DEVELOPER_ORGANIZATION) 65 | organizationUrl.set(POM_DEVELOPER_ORGANIZATION_URL) 66 | } 67 | } 68 | } 69 | 70 | artifact(emptyJavadocJar.get()) 71 | } 72 | 73 | repositories { 74 | 75 | val sonatypeUsername = System.getenv("SONATYPE_USERNAME") ?: "" 76 | val sonatypePassword = System.getenv("SONATYPE_PASSWORD") ?: "" 77 | 78 | maven { 79 | url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 80 | credentials { 81 | username = sonatypeUsername.ifBlank { "" } 82 | password = sonatypePassword.ifBlank { "" } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Versions.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | 3 | const val KOTLINX_COLLECTIONS_IMMUTABLE_VERSION = "0.3.5" 4 | 5 | const val KOR_VERSION = "5.3.0" 6 | } 7 | -------------------------------------------------------------------------------- /cobalt.core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Libraries.KORGE_FOUNDATION 2 | import Libraries.KOTLINX_COLLECTIONS_IMMUTABLE 3 | import Libraries.KOTLIN_REFLECT 4 | 5 | plugins { 6 | kotlin("multiplatform") 7 | id("maven-publish") 8 | id("signing") 9 | } 10 | 11 | val javaVersion = JavaVersion.VERSION_11 12 | 13 | java { 14 | sourceCompatibility = javaVersion 15 | targetCompatibility = javaVersion 16 | } 17 | 18 | kotlin { 19 | 20 | jvm { 21 | withJava() 22 | compilations.all { 23 | kotlinOptions { 24 | apiVersion = "1.9" 25 | languageVersion = "1.9" 26 | jvmTarget = javaVersion.toString() 27 | 28 | } 29 | } 30 | } 31 | 32 | js(IR) { 33 | compilations.all { 34 | kotlinOptions { 35 | sourceMap = true 36 | moduleKind = "umd" 37 | metaInfo = true 38 | } 39 | } 40 | browser { 41 | testTask { 42 | useMocha() 43 | } 44 | } 45 | nodejs() 46 | } 47 | 48 | sourceSets { 49 | val commonMain by getting { 50 | dependencies { 51 | api(KOTLIN_REFLECT) 52 | api(KOTLINX_COLLECTIONS_IMMUTABLE) 53 | 54 | api(KORGE_FOUNDATION) 55 | } 56 | } 57 | val commonTest by getting { 58 | dependencies { 59 | implementation(kotlin("test")) 60 | } 61 | } 62 | } 63 | 64 | } 65 | 66 | publishing { 67 | publishWith( 68 | project = project, 69 | module = "cobalt.core", 70 | desc = "Multiplatform utilities library for Kotlin." 71 | ) 72 | } 73 | 74 | signing { 75 | isRequired = false 76 | sign(publishing.publications) 77 | } 78 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/api/UUID.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.api 2 | 3 | import org.hexworks.cobalt.core.internal.DefaultUUID 4 | 5 | /** 6 | * Represents a unique identifier (UUID). 7 | */ 8 | interface UUID { 9 | 10 | companion object { 11 | 12 | /** 13 | * Creates a random [UUID]. 14 | */ 15 | fun randomUUID(): UUID = DefaultUUID.randomDefaultUUID() 16 | 17 | /** 18 | * Tries to create a [UUID] from a [String]. 19 | * This will throw an exception if the [UUID] cannot be created. 20 | */ 21 | fun fromString(str: String): UUID = DefaultUUID(str) 22 | } 23 | } 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/api/behavior/Disposable.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.api.behavior 2 | 3 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 4 | import org.hexworks.cobalt.events.api.Subscription 5 | 6 | /** 7 | * Represents an object which holds resources and can be [dispose]d 8 | * to free the resources used. 9 | */ 10 | interface Disposable { 11 | 12 | /** 13 | * Tells whether this [Disposable] is disposed or not. 14 | */ 15 | val disposed: Boolean 16 | get() = disposeState.isDisposed 17 | 18 | /** 19 | * Contains detailed information about the disposed state of this [Disposable]. 20 | */ 21 | val disposeState: DisposeState 22 | 23 | /** 24 | * Disposes this [Disposable] with the given [DisposeState]. 25 | * Default is [DisposedByHand]. 26 | */ 27 | fun dispose(disposeState: DisposeState = DisposedByHand) 28 | 29 | /** 30 | * Disposes this [Disposable] when the value of [condition] becomes `true`. 31 | * Will immediately [dispose] this [Disposable] if [ObservableValue.value] is `true`. 32 | */ 33 | infix fun disposeWhen(condition: ObservableValue) { 34 | if (condition.value) { 35 | dispose() 36 | } else { 37 | var subscription: Subscription? = null 38 | subscription = condition.onChange { (_, newValue) -> 39 | if (newValue) { 40 | subscription?.dispose() 41 | dispose() 42 | } 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Keeps this [Disposable] [NotDisposed] until [condition] becomes `false`. 49 | * Will immediately [dispose] this [Disposable] if [ObservableValue.value] is `false`. 50 | */ 51 | infix fun keepWhile(condition: ObservableValue) { 52 | if (condition.value.not()) { 53 | dispose() 54 | } else { 55 | var subscription: Subscription? = null 56 | subscription = condition.onChange { (_, newValue) -> 57 | if (newValue.not()) { 58 | subscription?.dispose() 59 | dispose() 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/api/behavior/DisposeState.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.api.behavior 2 | 3 | /** 4 | * Contains information about whether a [Disposable] object is disposed or not. 5 | */ 6 | sealed class DisposeState(val isDisposed: Boolean) { 7 | companion object 8 | } 9 | 10 | /** 11 | * The [Disposable] is not disposed. 12 | */ 13 | object NotDisposed : DisposeState(false) 14 | 15 | /** 16 | * The [Disposable] was disposed by the user. 17 | */ 18 | object DisposedByHand : DisposeState(true) 19 | 20 | /** 21 | * The [Disposable] was disposed by some event. 22 | */ 23 | data class DisposedByEvent(val event: T) : DisposeState(true) 24 | 25 | /** 26 | * The [Disposable] was disposed due to an [Exception]. 27 | */ 28 | data class DisposedByException(val exception: Exception) : DisposeState(true) 29 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/api/extensions/Functions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.api.extensions 2 | 3 | /** 4 | * Creates an identity function for [T]. 5 | */ 6 | fun identity(): (T) -> T = { it } 7 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/api/extensions/IdentifierExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.api.extensions 2 | 3 | import org.hexworks.cobalt.core.api.UUID 4 | 5 | /** 6 | * Returns the first 4 characters of this [UUID]. 7 | */ 8 | fun UUID.abbreviate() = toString().substring(0, 4) 9 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/api/extensions/Predicates.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.api.extensions 2 | 3 | typealias Predicate = Function1 4 | 5 | infix fun Predicate.and(other: Predicate): Predicate = { 6 | this(it) && other(it) 7 | } 8 | 9 | fun Predicate.negate(): Predicate = { 10 | !this(it) 11 | } 12 | 13 | infix fun Predicate.or(other: Predicate): Predicate = { 14 | this(it) || other(it) 15 | } 16 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/internal/Atom.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.internal 2 | 3 | import org.hexworks.cobalt.core.internal.impl.DefaultAtom 4 | 5 | internal interface Atom { 6 | 7 | fun get(): T 8 | 9 | fun transform(transformer: (T) -> T): T 10 | 11 | companion object { 12 | 13 | fun fromObject(obj: T): Atom = DefaultAtom(obj) 14 | } 15 | } 16 | 17 | internal fun T.toAtom(): Atom = Atom.fromObject(this) 18 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/internal/DefaultUUID.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.internal 2 | 3 | import org.hexworks.cobalt.core.api.UUID 4 | import kotlin.random.Random 5 | 6 | 7 | @Suppress("EXPERIMENTAL_API_USAGE") 8 | @OptIn(ExperimentalStdlibApi::class, ExperimentalUnsignedTypes::class) 9 | class DefaultUUID(val data: UByteArray) : UUID { 10 | override fun equals(other: Any?): Boolean = other is DefaultUUID && this.data.contentEquals(other.data) 11 | override fun hashCode(): Int = this.data.contentHashCode() 12 | 13 | companion object { 14 | private const val HEX = "0123456789ABCDEF" 15 | 16 | private val regex = 17 | Regex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", RegexOption.IGNORE_CASE) 18 | 19 | val NIL: UUID get() = DefaultUUID("00000000-0000-0000-0000-000000000000") 20 | 21 | private fun fix(data: UByteArray, version: Int, variant: Int): UByteArray { 22 | data[6] = ((data[6].toInt() and 0b0000_1111) or (version shl 4)).toUByte() 23 | data[8] = ((data[8].toInt() and 0x00_111111) or (variant shl 6)).toUByte() 24 | return data 25 | } 26 | 27 | fun randomDefaultUUID(random: Random = Random): UUID = DefaultUUID(fix(UByteArray(16).apply { 28 | random.nextBytes(this.asByteArray()) 29 | }, version = 4, variant = 1)) 30 | 31 | operator fun invoke(str: String): UUID { 32 | if (regex.matchEntire(str) == null) throw IllegalArgumentException("Invalid DefaultUUID") 33 | return DefaultUUID(str.replace("-", "").hexToUByteArray()) 34 | } 35 | } 36 | 37 | val version: Int get() = (data[6].toInt() ushr 4) and 0b1111 38 | val variant: Int get() = (data[8].toInt() ushr 6) and 0b11 39 | 40 | override fun toString(): String = buildString(36) { 41 | for (n in 0 until 16) { 42 | val c = data[n].toInt() 43 | append(HEX[c shr 4]) 44 | append(HEX[c and 0xF]) 45 | if (n == 3 || n == 5 || n == 7 || n == 9) append('-') 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/core/internal/impl/DefaultAtom.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.internal.impl 2 | 3 | import org.hexworks.cobalt.core.internal.Atom 4 | 5 | class DefaultAtom(initialValue: T) : Atom { 6 | 7 | private var value: T = initialValue 8 | 9 | override fun get(): T = value 10 | 11 | override fun transform(transformer: (T) -> T): T { 12 | value = transformer(value) 13 | return value 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/Cobalt.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api 2 | 3 | import org.hexworks.cobalt.events.api.EventBus 4 | 5 | object Cobalt { 6 | 7 | val eventbus: EventBus = EventBus.create() 8 | } 9 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/Binding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.binding 2 | 3 | import org.hexworks.cobalt.core.api.behavior.Disposable 4 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 5 | 6 | /** 7 | * A [Binding] computes its value based on the value of its dependencies. 8 | * A binding is subscribed to the changes of its dependencies and updates 9 | * its value whenever any of them changes. 10 | */ 11 | interface Binding : ObservableValue, Disposable { 12 | 13 | companion object 14 | } 15 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/BooleanBindings.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 4 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 5 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 6 | 7 | fun ObservableValue.bindNot(): Binding { 8 | return ComputedBinding(this) { it.not() } 9 | } 10 | 11 | infix fun ObservableValue.bindAndWith(other: ObservableValue): Binding { 12 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue && otherValue } 13 | } 14 | 15 | infix fun ObservableValue.bindOrWith(other: ObservableValue): Binding { 16 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue || otherValue } 17 | } 18 | 19 | infix fun ObservableValue.bindXorWith(other: ObservableValue): Binding { 20 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue xor otherValue } 21 | } 22 | 23 | infix fun ObservableValue.bindEqualsWith(other: ObservableValue): Binding { 24 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue } 25 | } 26 | 27 | fun ObservableValue.bindToString(): Binding { 28 | return ComputedBinding(this) { it.toString() } 29 | } 30 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/CollectionBindings.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.binding 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import kotlinx.collections.immutable.PersistentSet 5 | import kotlinx.collections.immutable.persistentListOf 6 | import kotlinx.collections.immutable.persistentSetOf 7 | import org.hexworks.cobalt.databinding.api.collection.ObservableList 8 | import org.hexworks.cobalt.databinding.api.collection.ObservableListBinding 9 | import org.hexworks.cobalt.databinding.api.collection.ObservableSet 10 | import org.hexworks.cobalt.databinding.api.collection.ObservableSetBinding 11 | import org.hexworks.cobalt.databinding.api.extension.ObservablePersistentCollection 12 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 13 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 14 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 15 | import org.hexworks.cobalt.databinding.internal.collections.ListBindingDecorator 16 | import org.hexworks.cobalt.databinding.internal.collections.SetBindingDecorator 17 | 18 | fun ObservablePersistentCollection.bindSize(): Binding { 19 | return ComputedBinding(this) { value -> 20 | value.size 21 | } 22 | } 23 | 24 | fun ObservableList>.bindFlatten(): ObservableListBinding { 25 | return ListBindingDecorator( 26 | ComputedBinding(this) { value -> 27 | var result = persistentListOf() 28 | value.forEach { 29 | result = result.addAll(it.value) 30 | } 31 | result 32 | } 33 | ) 34 | } 35 | 36 | fun ObservableList>.bindFlatMap( 37 | converter: (T) -> R 38 | ): ObservableListBinding { 39 | return ListBindingDecorator( 40 | ComputedBinding(this) { value -> 41 | var result = persistentListOf() 42 | value.forEach { 43 | result = result.addAll(it.value.map(converter)) 44 | } 45 | result 46 | } 47 | ) 48 | } 49 | 50 | fun ObservableSet>.bindFlatten(): ObservableSetBinding { 51 | return SetBindingDecorator( 52 | ComputedBinding(this) { value -> 53 | var result = persistentSetOf() 54 | value.forEach { 55 | result = result.addAll(it.value) 56 | } 57 | result 58 | } 59 | ) 60 | } 61 | 62 | fun ObservableSet>.bindFlatMap( 63 | converter: (T) -> R 64 | ): ObservableSetBinding { 65 | return SetBindingDecorator( 66 | ComputedBinding(this) { value -> 67 | var result = persistentSetOf() 68 | value.forEach { 69 | result = result.addAll(it.value.map(converter)) 70 | } 71 | result 72 | } 73 | ) 74 | } 75 | 76 | fun ObservablePersistentCollection.bindIsEmpty(): Binding { 77 | return ComputedBinding(this) { value -> 78 | value.isEmpty() 79 | } 80 | } 81 | 82 | infix fun ObservablePersistentCollection.bindContainsWith( 83 | other: ObservableValue 84 | ): Binding { 85 | return ComputedDualBinding(this, other) { thisValue, otherValue -> 86 | thisValue.contains(otherValue) 87 | } 88 | } 89 | 90 | infix fun ObservablePersistentCollection.bindContainsAllWith( 91 | other: ObservablePersistentCollection 92 | ): Binding { 93 | return ComputedDualBinding(this, other) { thisValue, otherValue -> 94 | thisValue.containsAll(otherValue) 95 | } 96 | } 97 | 98 | infix fun ObservablePersistentCollection.bindIndexOfWith( 99 | other: ObservableValue 100 | ): Binding { 101 | return ComputedDualBinding(this, other) { thisValue, otherValue -> 102 | thisValue.indexOf(otherValue) 103 | } 104 | } 105 | 106 | infix fun ObservablePersistentCollection.bindLastIndexOfWith( 107 | other: ObservableValue 108 | ): Binding { 109 | return ComputedDualBinding(this, other) { thisValue, otherValue -> 110 | thisValue.lastIndexOf(otherValue) 111 | } 112 | } 113 | 114 | infix fun ObservablePersistentCollection.bindIsEqualToWith( 115 | other: ObservablePersistentCollection 116 | ): Binding { 117 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue } 118 | } 119 | 120 | infix fun ObservableValue>.bindPlusWith( 121 | other: ObservableValue> 122 | ): ObservableListBinding { 123 | return ListBindingDecorator( 124 | ComputedDualBinding(this, other) { thisValue, otherValue -> 125 | thisValue.addAll(otherValue) 126 | } 127 | ) 128 | } 129 | 130 | infix fun ObservableValue>.bindMinusWith( 131 | other: ObservableValue> 132 | ): ObservableListBinding { 133 | return ListBindingDecorator( 134 | ComputedDualBinding(this, other) { thisValue, otherValue -> 135 | thisValue.removeAll(otherValue) 136 | } 137 | ) 138 | } 139 | 140 | infix fun ObservableValue>.bindPlusWith( 141 | other: ObservableValue> 142 | ): ObservableSetBinding { 143 | return SetBindingDecorator( 144 | ComputedDualBinding(this, other) { thisValue, otherValue -> 145 | thisValue.addAll(otherValue) 146 | } 147 | ) 148 | } 149 | 150 | infix fun ObservableValue>.bindMinusWith( 151 | other: ObservableValue> 152 | ): ObservableSetBinding { 153 | return SetBindingDecorator( 154 | ComputedDualBinding(this, other) { thisValue, otherValue -> 155 | thisValue.removeAll(otherValue) 156 | } 157 | ) 158 | } 159 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/DoubleBindings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package org.hexworks.cobalt.databinding.api.binding 4 | 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 7 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 8 | 9 | fun ObservableValue.bindNegate(): Binding { 10 | return ComputedBinding(this) { -it } 11 | } 12 | 13 | infix fun ObservableValue.bindPlusWith(other: ObservableValue): Binding { 14 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue + otherValue.toDouble() } 15 | } 16 | 17 | infix fun ObservableValue.bindMinusWith(other: ObservableValue): Binding { 18 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue - otherValue.toDouble() } 19 | } 20 | 21 | infix fun ObservableValue.bindTimesWith(other: ObservableValue): Binding { 22 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue * otherValue.toDouble() } 23 | } 24 | 25 | infix fun ObservableValue.bindDivWith(other: ObservableValue): Binding { 26 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue / otherValue.toDouble() } 27 | } 28 | 29 | infix fun ObservableValue.bindEqualsWith(other: ObservableValue): Binding { 30 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue.toDouble() } 31 | } 32 | 33 | infix fun ObservableValue.bindGreaterThanWith(other: ObservableValue): Binding { 34 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue > otherValue.toDouble() } 35 | } 36 | 37 | infix fun ObservableValue.bindLessThanWith(other: ObservableValue): Binding { 38 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue < otherValue.toDouble() } 39 | } 40 | 41 | infix fun ObservableValue.bindGreaterThanOrEqualToWith(other: ObservableValue): Binding { 42 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue >= otherValue.toDouble() } 43 | } 44 | 45 | infix fun ObservableValue.bindLessThanOrEqualToWith(other: ObservableValue): Binding { 46 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue <= otherValue.toDouble() } 47 | } 48 | 49 | fun ObservableValue.bindToString(): Binding { 50 | return ComputedBinding(this) { it.toString() } 51 | } 52 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/FloatBindings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package org.hexworks.cobalt.databinding.api.binding 4 | 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 7 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 8 | 9 | fun ObservableValue.bindNegate(): Binding { 10 | return ComputedBinding(this) { -it } 11 | } 12 | 13 | infix fun ObservableValue.bindPlusWith(other: ObservableValue): Binding { 14 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue + otherValue.toFloat() } 15 | } 16 | 17 | infix fun ObservableValue.bindMinusWith(other: ObservableValue): Binding { 18 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue - otherValue.toFloat() } 19 | } 20 | 21 | infix fun ObservableValue.bindTimesWith(other: ObservableValue): Binding { 22 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue * otherValue.toFloat() } 23 | } 24 | 25 | infix fun ObservableValue.bindDivWith(other: ObservableValue): Binding { 26 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue / otherValue.toFloat() } 27 | } 28 | 29 | infix fun ObservableValue.bindEqualsWith(other: ObservableValue): Binding { 30 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue.toFloat() } 31 | } 32 | 33 | infix fun ObservableValue.bindGreaterThanWith(other: ObservableValue): Binding { 34 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue > otherValue.toFloat() } 35 | } 36 | 37 | infix fun ObservableValue.bindLessThanWith(other: ObservableValue): Binding { 38 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue < otherValue.toFloat() } 39 | } 40 | 41 | infix fun ObservableValue.bindGreaterThanOrEqualToWith(other: ObservableValue): Binding { 42 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue >= otherValue.toFloat() } 43 | } 44 | 45 | infix fun ObservableValue.bindLessThanOrEqualToWith(other: ObservableValue): Binding { 46 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue <= otherValue.toFloat() } 47 | } 48 | 49 | fun ObservableValue.bindToString(): Binding { 50 | return ComputedBinding(this) { it.toString() } 51 | } 52 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/GenericBindings.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.collection.ObservableList 4 | import org.hexworks.cobalt.databinding.api.collection.ObservableListBinding 5 | import org.hexworks.cobalt.databinding.api.collection.ObservableSet 6 | import org.hexworks.cobalt.databinding.api.collection.ObservableSetBinding 7 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 8 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 9 | import org.hexworks.cobalt.databinding.internal.binding.ListBinding 10 | import org.hexworks.cobalt.databinding.internal.binding.SetBinding 11 | import org.hexworks.cobalt.databinding.internal.collections.ListBindingDecorator 12 | import org.hexworks.cobalt.databinding.internal.collections.SetBindingDecorator 13 | 14 | /** 15 | * Creates a [Binding] which will contain the transformed value of this [ObservableValue] 16 | * from its type [S] to a new type [T]. 17 | */ 18 | fun ObservableValue.bindTransform(transformer: (S) -> T): Binding { 19 | return ComputedBinding(this, transformer) 20 | } 21 | 22 | /** 23 | * Creates a [Binding] which will contain the mapped values of this [ObservableList] 24 | * from its type [S] to a new type [T]. 25 | */ 26 | fun ObservableList.bindMap(transformer: (S) -> T): ObservableListBinding { 27 | return ListBindingDecorator(ListBinding(this, transformer)) 28 | } 29 | 30 | /** 31 | * Creates a [Binding] which will contain the mapped values of this [ObservableList] 32 | * from its type [S] to a new type [T]. 33 | */ 34 | fun ObservableSet.bindMap(transformer: (S) -> T): ObservableSetBinding { 35 | return SetBindingDecorator(SetBinding(this, transformer)) 36 | } 37 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/IntBindings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package org.hexworks.cobalt.databinding.api.binding 4 | 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 7 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 8 | 9 | fun ObservableValue.bindNegate(): Binding { 10 | return ComputedBinding(this) { -it } 11 | } 12 | 13 | infix fun ObservableValue.bindPlusWith(other: ObservableValue): Binding { 14 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue + otherValue.toInt() } 15 | } 16 | 17 | infix fun ObservableValue.bindMinusWith(other: ObservableValue): Binding { 18 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue - otherValue.toInt() } 19 | } 20 | 21 | infix fun ObservableValue.bindTimesWith(other: ObservableValue): Binding { 22 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue * otherValue.toInt() } 23 | } 24 | 25 | infix fun ObservableValue.bindDivWith(other: ObservableValue): Binding { 26 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue / otherValue.toInt() } 27 | } 28 | 29 | infix fun ObservableValue.bindEqualsWith(other: ObservableValue): Binding { 30 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue.toInt() } 31 | } 32 | 33 | infix fun ObservableValue.bindGreaterThanWith(other: ObservableValue): Binding { 34 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue > otherValue.toInt() } 35 | } 36 | 37 | infix fun ObservableValue.bindLessThanWith(other: ObservableValue): Binding { 38 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue < otherValue.toInt() } 39 | } 40 | 41 | infix fun ObservableValue.bindGreaterThanOrEqualToWith(other: ObservableValue): Binding { 42 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue >= otherValue.toInt() } 43 | } 44 | 45 | infix fun ObservableValue.bindLessThanOrEqualToWith(other: ObservableValue): Binding { 46 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue <= otherValue.toInt() } 47 | } 48 | 49 | fun ObservableValue.bindToString(): Binding { 50 | return ComputedBinding(this) { it.toString() } 51 | } 52 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/LongBindings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package org.hexworks.cobalt.databinding.api.binding 4 | 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 7 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 8 | 9 | fun ObservableValue.bindNegate(): Binding { 10 | return ComputedBinding(this) { -it } 11 | } 12 | 13 | infix fun ObservableValue.bindPlusWith(other: ObservableValue): Binding { 14 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue + otherValue.toLong() } 15 | } 16 | 17 | infix fun ObservableValue.bindMinusWith(other: ObservableValue): Binding { 18 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue - otherValue.toLong() } 19 | } 20 | 21 | infix fun ObservableValue.bindTimesWith(other: ObservableValue): Binding { 22 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue * otherValue.toLong() } 23 | } 24 | 25 | infix fun ObservableValue.bindDivWith(other: ObservableValue): Binding { 26 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue / otherValue.toLong() } 27 | } 28 | 29 | infix fun ObservableValue.bindEqualsWith(other: ObservableValue): Binding { 30 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue.toLong() } 31 | } 32 | 33 | infix fun ObservableValue.bindGreaterThanWith(other: ObservableValue): Binding { 34 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue > otherValue.toLong() } 35 | } 36 | 37 | infix fun ObservableValue.bindLessThanWith(other: ObservableValue): Binding { 38 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue < otherValue.toLong() } 39 | } 40 | 41 | infix fun ObservableValue.bindGreaterThanOrEqualToWith(other: ObservableValue): Binding { 42 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue >= otherValue.toLong() } 43 | } 44 | 45 | infix fun ObservableValue.bindLessThanOrEqualToWith(other: ObservableValue): Binding { 46 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue <= otherValue.toLong() } 47 | } 48 | 49 | fun ObservableValue.bindToString(): Binding { 50 | return ComputedBinding(this) { it.toString() } 51 | } 52 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/binding/StringBindings.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.converter.toConverter 4 | import org.hexworks.cobalt.databinding.api.extension.toInternalProperty 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.internal.binding.ComputedBinding 7 | import org.hexworks.cobalt.databinding.internal.binding.ComputedDualBinding 8 | import org.hexworks.cobalt.databinding.internal.binding.UnidirectionalBinding 9 | 10 | fun ObservableValue.bindIsEmpty(): Binding { 11 | val converter = { str: String -> str.isEmpty() }.toConverter() 12 | return UnidirectionalBinding( 13 | source = this, 14 | target = converter.convert(this.value).toInternalProperty(), 15 | converter = converter 16 | ) 17 | } 18 | 19 | fun ObservableValue.bindIsBlank(): Binding { 20 | val converter = { str: String -> str.isBlank() }.toConverter() 21 | return UnidirectionalBinding( 22 | source = this, 23 | target = converter.convert(this.value).toInternalProperty(), 24 | converter = converter 25 | ) 26 | } 27 | 28 | infix fun ObservableValue.bindPlusWith(other: ObservableValue): Binding { 29 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue + otherValue } 30 | } 31 | 32 | infix fun ObservableValue.bindEqualsWith(other: ObservableValue): Binding { 33 | return ComputedDualBinding(this, other) { thisValue, otherValue -> thisValue == otherValue } 34 | } 35 | 36 | infix fun ObservableValue.bindEqualsIgnoreCase(other: ObservableValue): Binding { 37 | return ComputedDualBinding( 38 | this, 39 | other 40 | ) { thisValue, otherValue -> thisValue.lowercase() == otherValue.lowercase() } 41 | } 42 | 43 | fun ObservableValue.length(): Binding { 44 | return ComputedBinding(this) { it.length } 45 | } 46 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/CollectionProperty.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentCollection 4 | import org.hexworks.cobalt.databinding.api.property.Property 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.api.value.WritableValue 7 | 8 | /** 9 | * A [CollectionProperty] is a [Property] which wraps an underlying [PersistentCollection]. 10 | * @see Property 11 | * @see ObservableValue 12 | * @see WritableValue 13 | */ 14 | interface CollectionProperty> : 15 | ObservableCollection, WritableCollection, Property { 16 | 17 | companion object 18 | } 19 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ListProperty.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import org.hexworks.cobalt.databinding.api.property.Property 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.api.value.WritableValue 7 | 8 | /** 9 | * A [ListProperty] is a [Property] which wraps an underlying [PersistentList]. 10 | * @see Property 11 | * @see ObservableValue 12 | * @see WritableValue 13 | */ 14 | interface ListProperty : 15 | ObservableList, 16 | WritableList, 17 | ObservableCollection>, 18 | Property> { 19 | 20 | companion object 21 | } 22 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/MapProperty.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentMap 4 | import org.hexworks.cobalt.databinding.api.property.Property 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.api.value.WritableValue 7 | 8 | /** 9 | * A [MapProperty] is a [Property] which wraps an underlying [PersistentMap]. 10 | * @see Property 11 | * @see ObservableValue 12 | * @see WritableValue 13 | */ 14 | interface MapProperty : 15 | ObservableMap, 16 | WritableMap, 17 | Property> { 18 | 19 | companion object 20 | } 21 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ObservableCollection.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentCollection 4 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 5 | 6 | /** 7 | * An [ObservableCollection] is an [ObservableValue] which wraps an underlying 8 | * [PersistentCollection] and can be used to track its changes with [onChange]. 9 | */ 10 | interface ObservableCollection> : PersistentCollection, ObservableValue { 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ObservableList.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 5 | 6 | /** 7 | * An [ObservableList] is an [ObservableValue] which wraps an underlying 8 | * [PersistentList] and can be used to track its changes with [onChange]. 9 | */ 10 | interface ObservableList : 11 | ObservableCollection>, 12 | ObservableValue>, 13 | PersistentList { 14 | 15 | companion object 16 | } 17 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ObservableListBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import org.hexworks.cobalt.databinding.api.binding.Binding 5 | 6 | interface ObservableListBinding : ObservableList, Binding> 7 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ObservableMap.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentMap 4 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 5 | 6 | /** 7 | * An [ObservableMap] is an [ObservableValue] which wraps an underlying 8 | * [PersistentMap] and can be used to track its changes with [onChange]. 9 | */ 10 | interface ObservableMap : PersistentMap, ObservableValue> { 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ObservableSet.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import kotlinx.collections.immutable.PersistentSet 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | 7 | /** 8 | * An [ObservableSet] is an [ObservableValue] which wraps an underlying 9 | * [PersistentList] and can be used to track its changes with [onChange]. 10 | */ 11 | interface ObservableSet : ObservableCollection>, PersistentSet { 12 | 13 | companion object 14 | } 15 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/ObservableSetBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentSet 4 | import org.hexworks.cobalt.databinding.api.binding.Binding 5 | 6 | interface ObservableSetBinding : ObservableSet, Binding> 7 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/SetProperty.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentSet 4 | import org.hexworks.cobalt.databinding.api.property.Property 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.api.value.WritableValue 7 | 8 | /** 9 | * A [SetProperty] is a [Property] which wraps an underlying [PersistentSet]. 10 | * @see Property 11 | * @see ObservableValue 12 | * @see WritableValue 13 | */ 14 | interface SetProperty : 15 | ObservableSet, 16 | WritableSet, 17 | ObservableCollection>, 18 | Property> { 19 | 20 | companion object 21 | } 22 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/WritableCollection.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentCollection 4 | import org.hexworks.cobalt.databinding.api.value.Value 5 | import org.hexworks.cobalt.databinding.api.value.WritableValue 6 | 7 | /** 8 | * A [WritableCollection] is a [Value] which wraps an underlying [PersistentCollection] 9 | * and allows changing its [value]. 10 | */ 11 | interface WritableCollection> : WritableValue { 12 | 13 | companion object 14 | } 15 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/WritableList.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import org.hexworks.cobalt.databinding.api.value.Value 5 | 6 | /** 7 | * A [WritableList] is a [Value] which wraps an underlying [PersistentList] 8 | * and allows changing its [value]. 9 | */ 10 | interface WritableList : WritableCollection> { 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/WritableMap.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import kotlinx.collections.immutable.PersistentMap 5 | import org.hexworks.cobalt.databinding.api.value.Value 6 | import org.hexworks.cobalt.databinding.api.value.WritableValue 7 | 8 | /** 9 | * A [WritableMap] is a [Value] which wraps an underlying [PersistentList] 10 | * and allows changing its [value]. 11 | */ 12 | interface WritableMap : PersistentMap, WritableValue> { 13 | 14 | companion object 15 | } 16 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/collection/WritableSet.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.collection 2 | 3 | import kotlinx.collections.immutable.PersistentSet 4 | import org.hexworks.cobalt.databinding.api.value.Value 5 | 6 | /** 7 | * A [WritableSet] is a [Value] which wraps an underlying [PersistentSet] 8 | * and allows changing its [value]. 9 | */ 10 | interface WritableSet : WritableCollection> { 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/converter/Converter.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.converter 2 | 3 | /** 4 | * Allows for converting a value [S] to a value [T]. This interface 5 | * (and its derivatives) are needed because in MPP one can't use plain 6 | * lambdas for this purpose because Javascript doesn't support it. 7 | * Use [((S) -> T).toConverter()] to create a [Converter] form a lambda. 8 | */ 9 | interface Converter { 10 | 11 | fun convert(source: S): T 12 | 13 | companion object 14 | } 15 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/converter/Converters.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.converter 2 | 3 | fun ((S) -> T).toConverter() = object : Converter { 4 | override fun convert(source: S) = invoke(source) 5 | } 6 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/converter/IdentityConverter.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.converter 2 | 3 | class IdentityConverter : IsomorphicConverter { 4 | 5 | override fun convert(source: T) = source 6 | 7 | override fun convertBack(target: T) = target 8 | } 9 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/converter/IsomorphicConverter.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.converter 2 | 3 | /** 4 | * Responsible for converting a value of type [S] to a value of type [T] 5 | * and vice versa. 6 | * @see Converter 7 | */ 8 | interface IsomorphicConverter : Converter { 9 | 10 | /** 11 | * Converts [target] to an object of type [S]. 12 | */ 13 | fun convertBack(target: T): S 14 | 15 | /** 16 | * Returns a [Converter] where the type parameters [S] and [T] are swapped. 17 | */ 18 | fun reverseConverter(): Converter = object : Converter { 19 | 20 | override fun convert(source: T): S { 21 | return convertBack(source) 22 | } 23 | } 24 | 25 | companion object 26 | } 27 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/event/ObservableValueChanged.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.event 2 | 3 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 4 | import org.hexworks.cobalt.events.api.Event 5 | 6 | /** 7 | * Fired when an [ObservableValue] **changes** its value. 8 | */ 9 | data class ObservableValueChanged( 10 | val oldValue: T, 11 | val newValue: T, 12 | val observableValue: ObservableValue, 13 | val type: ChangeType, 14 | override val emitter: Any, 15 | override val trace: Iterable = listOf() 16 | ) : Event { 17 | companion object 18 | } 19 | 20 | sealed class ChangeType 21 | object ScalarChange : ChangeType() { 22 | override fun toString() = "ChangeType.ScalarChange" 23 | } 24 | 25 | sealed class ListChange : ChangeType() 26 | data class ListAdd(val element: T) : ListChange() 27 | data class ListAddAt(val index: Int, val element: T) : ListChange() 28 | data class ListRemove(val element: T) : ListChange() 29 | data class ListRemoveAt(val index: Int) : ListChange() 30 | data class ListSet(val index: Int, val element: T) : ListChange() 31 | data class ListAddAll(val elements: Collection) : ListChange() 32 | data class ListAddAllAt(val index: Int, val c: Collection) : ListChange() 33 | data class ListRemoveAll(val elements: Collection) : ListChange() 34 | data class ListRemoveAllWhen(val predicate: (T) -> Boolean) : ListChange() 35 | data class ListRetainAll(val elements: Collection) : ListChange() 36 | data class ListPropertyChange(val changeEvent: ObservableValueChanged) : ListChange() 37 | object ListClear : ListChange() { 38 | override fun toString() = "ChangeType.ListChange.ListClear" 39 | } 40 | 41 | sealed class SetChange : ChangeType() 42 | data class SetAdd(val element: T) : SetChange() 43 | data class SetRemove(val element: T) : SetChange() 44 | data class SetAddAll(val elements: Collection) : SetChange() 45 | data class SetRemoveAll(val elements: Collection) : SetChange() 46 | data class SetRemoveAllWhen(val predicate: (T) -> Boolean) : SetChange() 47 | data class SetRetainAll(val elements: Collection) : SetChange() 48 | data class SetPropertyChange(val changeEvent: ObservableValueChanged) : SetChange() 49 | object SetClear : SetChange() { 50 | override fun toString() = "ChangeType.SetChange.SetClear" 51 | } 52 | 53 | sealed class MapChange : ChangeType() 54 | data class MapPut(val key: K, val value: V) : MapChange() 55 | data class MapPutAll(val m: Map) : MapChange() 56 | data class MapRemove(val key: K) : MapChange() 57 | data class MapRemoveWithValue(val key: K, val value: V) : MapChange() 58 | data class MapPropertyChange(val changeEvent: ObservableValueChanged) : MapChange() 59 | object MapClear : MapChange() { 60 | override fun toString() = "ChangeType.MapChange.MapClear" 61 | } 62 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/extension/AnyExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.extension 2 | 3 | /** 4 | * Returns the value supplied by [whenNull] if [T] is `null`, 5 | * otherwise returns the result of applying [whenNotNull] to the value. 6 | */ 7 | fun T?.fold( 8 | whenNull: () -> R, 9 | whenNotNull: (T) -> R 10 | ): R = this?.let(whenNotNull) ?: whenNull() 11 | 12 | /** 13 | * Returns `this` if it is not `null`, otherwise 14 | * returns [other]. 15 | */ 16 | fun T?.orElse(other: T): T = this ?: other 17 | 18 | /** 19 | * Returns `this` if it is not `null`, otherwise 20 | * returns the result of calling [other]. 21 | */ 22 | fun T?.orElseGet(other: () -> T): T = this ?: other() 23 | 24 | /** 25 | * Returns the value of `this` if it is not `null`, or throws 26 | * the exception that's returned by [exceptionSupplier]. 27 | */ 28 | fun T?.orElseThrow( 29 | exceptionSupplier: () -> X 30 | ): T = this ?: throw exceptionSupplier() 31 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/extension/ListExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.extension 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import kotlinx.collections.immutable.persistentListOf 5 | 6 | inline fun PersistentList.map(transform: (T) -> R): PersistentList { 7 | var result = persistentListOf() 8 | forEach { result = result.add(transform(it)) } 9 | return result 10 | } -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/extension/Properties.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Properties") 2 | 3 | package org.hexworks.cobalt.databinding.api.extension 4 | 5 | import kotlinx.collections.immutable.toPersistentList 6 | import kotlinx.collections.immutable.toPersistentMap 7 | import kotlinx.collections.immutable.toPersistentSet 8 | import org.hexworks.cobalt.databinding.api.collection.ListProperty 9 | import org.hexworks.cobalt.databinding.api.collection.MapProperty 10 | import org.hexworks.cobalt.databinding.api.collection.SetProperty 11 | import org.hexworks.cobalt.databinding.api.property.Property 12 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 13 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 14 | import org.hexworks.cobalt.databinding.internal.collections.DefaultListProperty 15 | import org.hexworks.cobalt.databinding.internal.collections.DefaultMapProperty 16 | import org.hexworks.cobalt.databinding.internal.collections.DefaultPropertyListProperty 17 | import org.hexworks.cobalt.databinding.internal.collections.DefaultPropertyMapProperty 18 | import org.hexworks.cobalt.databinding.internal.collections.DefaultPropertySetProperty 19 | import org.hexworks.cobalt.databinding.internal.collections.DefaultSetProperty 20 | import org.hexworks.cobalt.databinding.internal.property.DefaultProperty 21 | import org.hexworks.cobalt.databinding.internal.property.InternalProperty 22 | import kotlin.jvm.JvmName 23 | import kotlin.jvm.JvmOverloads 24 | 25 | /** 26 | * Creates a new [Property] from the given object of type [T]. 27 | */ 28 | @JvmOverloads 29 | fun T.toProperty( 30 | validator: PropertyValidator = { _, _ -> true }, 31 | optionalName: String? = null, 32 | ): Property = DefaultProperty(this, optionalName, validator) 33 | 34 | // Iterable / Collection 35 | 36 | @JvmName("toIterableProperty") 37 | @JvmOverloads 38 | fun Iterable.toProperty( 39 | validator: PropertyValidator> = { _, _ -> true }, 40 | optionalName: String? = null, 41 | ): ListProperty = DefaultListProperty(this.toPersistentList(), optionalName, validator) 42 | 43 | @JvmName("toCollectionProperty") 44 | @JvmOverloads 45 | fun Collection.toProperty( 46 | validator: PropertyValidator> = { _, _ -> true }, 47 | optionalName: String? = null, 48 | ): ListProperty = DefaultListProperty(this.toPersistentList(), optionalName, validator) 49 | 50 | // List 51 | @JvmName("toListProperty") 52 | @JvmOverloads 53 | fun List.toProperty( 54 | validator: PropertyValidator> = { _, _ -> true }, 55 | optionalName: String? = null, 56 | ): ListProperty = DefaultListProperty(this.toPersistentList(), optionalName, validator) 57 | 58 | @JvmName("toPropertyListProperty") 59 | @JvmOverloads 60 | fun > List.toProperty( 61 | validator: PropertyValidator> = { _, _ -> true }, 62 | optionalName: String? = null, 63 | ): ListProperty = DefaultPropertyListProperty(this.toPersistentList(), optionalName, validator) 64 | 65 | // Map 66 | @JvmName("toMapProperty") 67 | @JvmOverloads 68 | fun Map.toProperty( 69 | validator: PropertyValidator> = { _, _ -> true }, 70 | optionalName: String? = null, 71 | ): MapProperty = DefaultMapProperty(this.toPersistentMap(), optionalName, validator) 72 | 73 | @JvmName("toPropertyMapProperty") 74 | @JvmOverloads 75 | fun > Map.toProperty( 76 | validator: PropertyValidator> = { _, _ -> true }, 77 | optionalName: String? = null, 78 | ): MapProperty = DefaultPropertyMapProperty(this.toPersistentMap(), optionalName, validator) 79 | 80 | @JvmName("toSetProperty") 81 | @JvmOverloads 82 | fun Set.toProperty( 83 | validator: PropertyValidator> = { _, _ -> true }, 84 | optionalName: String? = null, 85 | ): SetProperty = DefaultSetProperty(this.toPersistentSet(), optionalName, validator) 86 | 87 | @JvmName("toPropertySetProperty") 88 | @JvmOverloads 89 | fun > Set.toProperty( 90 | validator: PropertyValidator> = { _, _ -> true }, 91 | optionalName: String? = null, 92 | ): SetProperty = DefaultPropertySetProperty(this.toPersistentSet(), optionalName, validator) 93 | 94 | /** 95 | * Creates a new [InternalProperty] from the given object of type [T]. 96 | */ 97 | internal fun T.toInternalProperty( 98 | validator: PropertyValidator = { _, _ -> true }, 99 | optionalName: String? = null, 100 | ): InternalProperty = DefaultProperty(this, optionalName, validator) 101 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/extension/Subscriptions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.extension 2 | 3 | import org.hexworks.cobalt.events.api.Subscription 4 | 5 | /** 6 | * Clears the [Subscription]s in this [MutableList] and also clears this list. 7 | */ 8 | fun MutableList.disposeSubscriptions() { 9 | forEach { 10 | try { 11 | it.dispose() 12 | } catch (e: Exception) { 13 | println("Cancelling subscription failed: ${e.message}.") 14 | } 15 | } 16 | clear() 17 | } 18 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/extension/TypeAliases.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.extension 2 | 3 | import kotlinx.collections.immutable.PersistentCollection 4 | import kotlinx.collections.immutable.PersistentList 5 | import kotlinx.collections.immutable.PersistentMap 6 | import kotlinx.collections.immutable.PersistentSet 7 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 8 | 9 | typealias ObservablePersistentCollection = ObservableValue> 10 | 11 | typealias ObservablePersistentList = ObservableValue> 12 | 13 | typealias ObservablePersistentSet = ObservableValue> 14 | 15 | typealias ObservablePersistentMap = ObservableValue> 16 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/property/Property.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.property 2 | 3 | import org.hexworks.cobalt.databinding.api.binding.Binding 4 | import org.hexworks.cobalt.databinding.api.converter.IsomorphicConverter 5 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 6 | import org.hexworks.cobalt.databinding.api.value.Value 7 | import org.hexworks.cobalt.databinding.api.value.WritableValue 8 | import org.hexworks.cobalt.databinding.internal.property.DefaultPropertyDelegate 9 | 10 | /** 11 | * A [Property] is a [Value] which can be *read*, *written* and *observed* 12 | * and also supports *bidirectional* binding using the [bind] function. 13 | * @see WritableValue 14 | * @see ObservableValue 15 | */ 16 | interface Property : WritableValue, ObservableValue { 17 | 18 | /** 19 | * Creates a bidirectional binding between this [Property] and [other]. 20 | * eg: whenever each one is updated, the other will get updated with 21 | * the new value. If [updateWhenBound] is `true` then the value of this 22 | * [Property] will be updated when the binding takes place. Otherwise it will 23 | * only get updated when [other] is next updated. 24 | */ 25 | fun bind( 26 | other: Property, 27 | updateWhenBound: Boolean = true 28 | ): Binding 29 | 30 | /** 31 | * Creates a bidirectional binding between this [Property] and [other]. 32 | * Uses the given [IsomorphicConverter] to convert the values between the subject properties. 33 | * If [updateWhenBound] is `true` then the value of this [WritableValue] will be updated 34 | * when the binding takes place. Otherwise it will only get updated when [other] is updated. 35 | */ 36 | fun bind( 37 | other: Property, 38 | updateWhenBound: Boolean = true, 39 | converter: IsomorphicConverter 40 | ): Binding 41 | 42 | /** 43 | * Creates a [PropertyDelegate] for this [Property]. 44 | */ 45 | fun asDelegate(): PropertyDelegate = DefaultPropertyDelegate(this) 46 | 47 | companion object 48 | } 49 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/property/PropertyDelegate.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.property 2 | 3 | import kotlin.properties.ReadWriteProperty 4 | 5 | /** 6 | * Augments [Property] with the ability to be used as a property delegate. 7 | */ 8 | interface PropertyDelegate : Property, ReadWriteProperty { 9 | 10 | companion object 11 | } 12 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/property/PropertyValidator.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.property 2 | 3 | typealias PropertyValidator = (oldValue: T, newValue: T) -> Boolean 4 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/value/ObservableValue.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.value 2 | 3 | import org.hexworks.cobalt.core.api.UUID 4 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 5 | import org.hexworks.cobalt.events.api.Subscription 6 | 7 | /** 8 | * An [ObservableValue] wraps a value and allows to observe the value for changes. 9 | */ 10 | interface ObservableValue : Value { 11 | 12 | val id: UUID 13 | val name: String 14 | 15 | /** 16 | * Starts observing this [ObservableValue] for changes. If it happens 17 | * [fn] will be called. **Note that** if the [value] of this [ObservableValue] 18 | * is **set** to the same value, the callback won't be called. 19 | */ 20 | fun onChange(fn: (ObservableValueChanged) -> Unit): Subscription 21 | 22 | companion object 23 | } 24 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/value/Value.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.value 2 | 3 | import org.hexworks.cobalt.databinding.api.property.Property 4 | 5 | /** 6 | * [Value] is an abstraction which can be used to wrap a [value]. 7 | * It serves as the basis for [ObservableValue] and [WritableValue] 8 | * which together form the [Property] abstraction. 9 | */ 10 | interface Value { 11 | 12 | val value: T 13 | 14 | companion object 15 | } 16 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/value/ValueValidationFailedException.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.value 2 | 3 | class ValueValidationFailedException( 4 | val newValue: Any?, 5 | message: String 6 | ) : RuntimeException(message) { 7 | 8 | companion object 9 | } 10 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/value/ValueValidationResult.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.value 2 | 3 | sealed class ValueValidationResult( 4 | val successful: Boolean 5 | ) { 6 | 7 | abstract val value: T 8 | 9 | companion object 10 | } 11 | 12 | data class ValueValidationFailed( 13 | override val value: T, 14 | val cause: ValueValidationFailedException 15 | ) : ValueValidationResult(false) 16 | 17 | data class ValueValidationSuccessful( 18 | override val value: T 19 | ) : ValueValidationResult(true) 20 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/api/value/WritableValue.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.api.value 2 | 3 | import org.hexworks.cobalt.databinding.api.binding.Binding 4 | 5 | /** 6 | * A [WritableValue] wraps a value which can be read and written. 7 | */ 8 | interface WritableValue : Value { 9 | 10 | override var value: T 11 | 12 | /** 13 | * Tries to update the [value] with the given [newValue]. 14 | * @return [ValueValidationSuccessful] if [newValue] is 15 | * acceptable, [ValueValidationFailed] if not. 16 | */ 17 | fun updateValue(newValue: T): ValueValidationResult 18 | 19 | /** 20 | * Tries to transform the [value] with the given [transformer] function. 21 | * It is guaranteed that no concurrent changes will happen to this [WritableValue] 22 | * while being transformed. 23 | * @return [ValueValidationSuccessful] if [newValue] is 24 | * acceptable, [ValueValidationFailed] if not. 25 | */ 26 | fun transformValue(transformer: (T) -> T): ValueValidationResult 27 | 28 | /** 29 | * Starts updating this [WritableValue] from the given [observable]. If [updateWhenBound] 30 | * is `true` then the value of this [WritableValue] will be updated when the binding takes place. 31 | * Otherwise, it will only get updated when [observable] is updated. 32 | * @return a [Binding] which can be disposed to stop the updates 33 | */ 34 | fun updateFrom( 35 | observable: ObservableValue, 36 | updateWhenBound: Boolean = true 37 | ): Binding 38 | 39 | /** 40 | * Starts updating this [WritableValue] from the given [observable]. 41 | * Uses the given [converter] to convert the values. If [updateWhenBound] 42 | * is `true` then the value of this [WritableValue] will be updated 43 | * when the binding takes place. Otherwise, it will only get updated when 44 | * [observable] is updated. 45 | * @return a [Binding] which can be disposed to stop the updates 46 | */ 47 | fun updateFrom( 48 | observable: ObservableValue, 49 | updateWhenBound: Boolean = true, 50 | converter: (S) -> T 51 | ): Binding 52 | 53 | companion object 54 | } 55 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/BaseBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.core.api.UUID 4 | import org.hexworks.cobalt.core.api.behavior.DisposeState 5 | import org.hexworks.cobalt.core.api.behavior.NotDisposed 6 | import org.hexworks.cobalt.databinding.api.Cobalt 7 | import org.hexworks.cobalt.databinding.api.binding.Binding 8 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 9 | import org.hexworks.cobalt.databinding.api.extension.disposeSubscriptions 10 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 11 | import org.hexworks.cobalt.databinding.internal.event.PropertyScope 12 | import org.hexworks.cobalt.databinding.internal.property.InternalProperty 13 | import org.hexworks.cobalt.events.api.Subscription 14 | import org.hexworks.cobalt.events.api.simpleSubscribeTo 15 | import org.hexworks.cobalt.logging.api.LoggerFactory 16 | 17 | abstract class BaseBinding( 18 | internal val source: ObservableValue, 19 | internal val target: InternalProperty, 20 | internal val subscriptions: MutableList 21 | ) : Binding { 22 | 23 | override val value: T 24 | get() { 25 | require(disposed.not()) { 26 | "Can't calculate the value of a Binding which is disposed." 27 | } 28 | return target.value 29 | } 30 | 31 | override var disposeState: DisposeState = NotDisposed 32 | internal set 33 | 34 | final override val id = UUID.randomUUID() 35 | 36 | internal val logger = LoggerFactory.getLogger(this::class) 37 | internal val propertyScope = PropertyScope(id) 38 | 39 | override fun dispose(disposeState: DisposeState) { 40 | this.disposeState = disposeState 41 | Cobalt.eventbus.cancelScope(propertyScope) 42 | subscriptions.disposeSubscriptions() 43 | } 44 | 45 | @Suppress("UNCHECKED_CAST") 46 | override fun onChange(fn: (ObservableValueChanged) -> Unit): Subscription { 47 | return Cobalt.eventbus.simpleSubscribeTo>(propertyScope) { 48 | fn(it) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/BidirectionalBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.Cobalt 4 | import org.hexworks.cobalt.databinding.api.binding.Binding 5 | import org.hexworks.cobalt.databinding.api.converter.IsomorphicConverter 6 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 7 | import org.hexworks.cobalt.databinding.internal.extensions.runWithDisposeOnFailure 8 | import org.hexworks.cobalt.databinding.internal.property.InternalProperty 9 | 10 | /** 11 | * Creates a **bidirectional** [Binding] between [source] and [target] which means that 12 | * whenever [source] gets updated [target] will be updated as well with the new value 13 | * and vice versa. 14 | * [converter] will be used to convert the values between [source] and [target]. 15 | */ 16 | internal class BidirectionalBinding( 17 | source: InternalProperty, 18 | target: InternalProperty, 19 | converter: IsomorphicConverter 20 | ) : BaseBinding( 21 | source = source, 22 | target = target, 23 | subscriptions = mutableListOf() 24 | ) { 25 | 26 | override val name = "BidirectionalBinding" 27 | 28 | private val reverseConverter = converter.reverseConverter() 29 | 30 | init { 31 | subscriptions.add( 32 | source.onChange { event -> 33 | runWithDisposeOnFailure { 34 | val oldValue = converter.convert(event.oldValue) 35 | val newValue = converter.convert(event.newValue) 36 | if (target.updateWithEvent( 37 | oldValue = oldValue, 38 | newValue = newValue, 39 | event = event 40 | ) 41 | ) { 42 | Cobalt.eventbus.publish( 43 | event = ObservableValueChanged( 44 | oldValue = oldValue, 45 | newValue = newValue, 46 | observableValue = this, 47 | emitter = this, 48 | trace = listOf(event) + event.trace, 49 | type = event.type 50 | ), 51 | eventScope = propertyScope 52 | ) 53 | } 54 | } 55 | } 56 | ) 57 | subscriptions.add( 58 | target.onChange { event -> 59 | runWithDisposeOnFailure { 60 | val oldValue = reverseConverter.convert(event.oldValue) 61 | val newValue = reverseConverter.convert(event.newValue) 62 | if (source.updateWithEvent( 63 | oldValue = oldValue, 64 | newValue = newValue, 65 | event = event 66 | ) 67 | ) { 68 | Cobalt.eventbus.publish( 69 | event = ObservableValueChanged( 70 | oldValue = oldValue, 71 | newValue = newValue, 72 | observableValue = this, 73 | emitter = this, 74 | trace = listOf(event) + event.trace, 75 | type = event.type 76 | ), 77 | eventScope = propertyScope 78 | ) 79 | } 80 | } 81 | } 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/ComputedBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.Cobalt 4 | import org.hexworks.cobalt.databinding.api.binding.Binding 5 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 6 | import org.hexworks.cobalt.databinding.api.event.ScalarChange 7 | import org.hexworks.cobalt.databinding.api.extension.toProperty 8 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 9 | import org.hexworks.cobalt.databinding.internal.property.InternalProperty 10 | 11 | /** 12 | * A [ComputedBinding] creates a [Binding] using an [ObservableValue]. The [Binding] will get 13 | * updated whenever the [ObservableValue] changes using [converter] to compute the new value of this [Binding]. 14 | */ 15 | @Suppress("UNCHECKED_CAST") 16 | class ComputedBinding( 17 | source: ObservableValue, 18 | converter: (S) -> T 19 | ) : BaseBinding( 20 | source = source, 21 | target = converter(source.value).toProperty() as InternalProperty, 22 | subscriptions = mutableListOf() 23 | ) { 24 | 25 | override val name = "ComputedBinding" 26 | 27 | init { 28 | subscriptions.add(source.onChange { event -> 29 | val oldValue = converter(event.oldValue) 30 | val newValue = converter(event.newValue) 31 | if (target.updateWithEvent( // TODO: we send 2 events, do we need to call this? 32 | oldValue = oldValue, 33 | newValue = newValue, 34 | event = event 35 | ) 36 | ) { 37 | Cobalt.eventbus.publish( 38 | event = ObservableValueChanged( 39 | oldValue = oldValue, 40 | newValue = newValue, 41 | observableValue = this, 42 | emitter = this, 43 | trace = listOf(event) + event.trace, 44 | type = ScalarChange 45 | ), 46 | eventScope = propertyScope 47 | ) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/ComputedDualBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.core.api.UUID 4 | import org.hexworks.cobalt.core.api.behavior.DisposeState 5 | import org.hexworks.cobalt.core.api.behavior.NotDisposed 6 | import org.hexworks.cobalt.databinding.api.Cobalt 7 | import org.hexworks.cobalt.databinding.api.binding.Binding 8 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 9 | import org.hexworks.cobalt.databinding.api.extension.disposeSubscriptions 10 | import org.hexworks.cobalt.databinding.api.extension.toInternalProperty 11 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 12 | import org.hexworks.cobalt.databinding.internal.event.PropertyScope 13 | import org.hexworks.cobalt.events.api.Subscription 14 | import org.hexworks.cobalt.events.api.simpleSubscribeTo 15 | 16 | /** 17 | * A [ComputedDualBinding] creates a [Binding] using **two** [ObservableValue]s which will get 18 | * updated whenever any of those values get updated using [computerFn] to compute the new value 19 | * of this [Binding]. 20 | */ 21 | internal class ComputedDualBinding( 22 | private val source0: ObservableValue, 23 | private val source1: ObservableValue, 24 | private val computerFn: (S0, S1) -> T 25 | ) : Binding { 26 | 27 | override val name = "ComputedDualBinding" 28 | 29 | private val target = computerFn(source0.value, source1.value).toInternalProperty() 30 | 31 | override val value: T 32 | get() { 33 | require(disposed.not()) { 34 | "Can't calculate the value of a Binding which is disposed." 35 | } 36 | return target.value 37 | } 38 | 39 | override var disposeState: DisposeState = 40 | NotDisposed 41 | internal set 42 | 43 | override val id = UUID.randomUUID() 44 | 45 | private val propertyScope = PropertyScope(id) 46 | private val subscriptions = mutableListOf() 47 | 48 | init { 49 | subscriptions.add( 50 | source0.onChange { event -> 51 | val oldValue = computerFn(event.oldValue, source1.value) 52 | val newValue = computerFn(event.newValue, source1.value) 53 | if (oldValue != newValue) { 54 | target.value = newValue 55 | Cobalt.eventbus.publish( 56 | event = ObservableValueChanged( 57 | oldValue = oldValue, 58 | newValue = newValue, 59 | observableValue = this, 60 | emitter = this, 61 | trace = listOf(event) + event.trace, 62 | type = event.type 63 | ), 64 | eventScope = propertyScope 65 | ) 66 | } 67 | } 68 | ) 69 | subscriptions.add( 70 | source1.onChange { event -> 71 | val oldValue = computerFn(source0.value, event.oldValue) 72 | val newValue = computerFn(source0.value, event.newValue) 73 | if (oldValue != newValue) { 74 | target.value = newValue 75 | Cobalt.eventbus.publish( 76 | event = ObservableValueChanged( 77 | oldValue = oldValue, 78 | newValue = newValue, 79 | observableValue = this, 80 | emitter = this, 81 | trace = listOf(event) + event.trace, 82 | type = event.type 83 | ), 84 | eventScope = propertyScope 85 | ) 86 | } 87 | } 88 | ) 89 | } 90 | 91 | override fun dispose(disposeState: DisposeState) { 92 | this.disposeState = disposeState 93 | Cobalt.eventbus.cancelScope(propertyScope) 94 | subscriptions.disposeSubscriptions() 95 | } 96 | 97 | override fun onChange(fn: (ObservableValueChanged) -> Unit): Subscription { 98 | return Cobalt.eventbus.simpleSubscribeTo>(propertyScope) { 99 | fn(it) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/ListBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import org.hexworks.cobalt.databinding.api.Cobalt 5 | import org.hexworks.cobalt.databinding.api.collection.ObservableList 6 | import org.hexworks.cobalt.databinding.api.event.* 7 | import org.hexworks.cobalt.databinding.api.extension.map 8 | import org.hexworks.cobalt.databinding.api.extension.toProperty 9 | import org.hexworks.cobalt.databinding.api.value.ValueValidationResult 10 | import org.hexworks.cobalt.databinding.internal.exception.CircularBindingException 11 | import org.hexworks.cobalt.databinding.internal.extensions.asInternalProperty 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | class ListBinding( 15 | source: ObservableList, 16 | converter: (S) -> T 17 | ) : BaseBinding, PersistentList>( 18 | source = source, 19 | target = source.value.map { converter(it) }.toProperty().asInternalProperty(), 20 | subscriptions = mutableListOf() 21 | ) { 22 | 23 | override val name = "ListBinding" 24 | 25 | init { 26 | subscriptions.add( 27 | source.onChange { event -> 28 | if (event.trace.any { it.emitter == this }) { 29 | throw CircularBindingException( 30 | "Circular binding detected with trace ${event.trace.joinToString()} for property $this. Loop was prevented." 31 | ) 32 | } 33 | val type = event.type 34 | val oldValue = target.value 35 | val maybeNewValue: ValueValidationResult> = target.transformValue { 36 | // This is not nice...but the thing is that in order to make ObservableList only emit ListChanges 37 | // in a type safe way we'd have to add it as a type parameter to the whole property abstraction 38 | // ...which would also break the API 39 | (type as? ListChange)?.let { 40 | when (type) { 41 | is ListAdd<*> -> oldValue.add(converter(type.element as S)) 42 | is ListAddAt<*> -> oldValue.add(type.index, converter(type.element as S)) 43 | is ListRemove<*> -> oldValue.remove(converter(type.element as S)) 44 | is ListRemoveAt -> oldValue.removeAt(type.index) 45 | is ListSet<*> -> oldValue.set(type.index, converter(type.element as S)) 46 | is ListAddAll<*> -> oldValue.addAll(type.elements.map { converter(it as S) }) 47 | is ListAddAllAt<*> -> oldValue.addAll(type.index, type.c.map { converter(it as S) }) 48 | is ListRemoveAll<*> -> oldValue.removeAll(type.elements.map { converter(it as S) }) 49 | is ListRemoveAllWhen<*> -> oldValue.removeAll { 50 | (type.predicate as (T) -> Boolean)(it) 51 | } 52 | is ListRetainAll<*> -> oldValue.retainAll(type.elements.map { converter(it as S) }) 53 | ListClear -> oldValue.clear() 54 | is ListPropertyChange<*> -> oldValue 55 | } 56 | } ?: oldValue 57 | } 58 | if (maybeNewValue.successful && oldValue != maybeNewValue.value) { 59 | Cobalt.eventbus.publish( 60 | event = ObservableValueChanged( 61 | oldValue = oldValue, 62 | newValue = maybeNewValue.value, 63 | observableValue = this, 64 | emitter = this, 65 | type = event.type, 66 | trace = listOf(event) + event.trace 67 | ), 68 | eventScope = propertyScope 69 | ) 70 | } 71 | } 72 | ) 73 | } 74 | 75 | override fun toString() = "${this::class.simpleName}(source=$source, target=$target)" 76 | } 77 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/SetBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import kotlinx.collections.immutable.PersistentSet 4 | import kotlinx.collections.immutable.persistentSetOf 5 | import org.hexworks.cobalt.databinding.api.Cobalt 6 | import org.hexworks.cobalt.databinding.api.collection.ObservableSet 7 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 8 | import org.hexworks.cobalt.databinding.api.event.SetAdd 9 | import org.hexworks.cobalt.databinding.api.event.SetAddAll 10 | import org.hexworks.cobalt.databinding.api.event.SetChange 11 | import org.hexworks.cobalt.databinding.api.event.SetClear 12 | import org.hexworks.cobalt.databinding.api.event.SetPropertyChange 13 | import org.hexworks.cobalt.databinding.api.event.SetRemove 14 | import org.hexworks.cobalt.databinding.api.event.SetRemoveAll 15 | import org.hexworks.cobalt.databinding.api.event.SetRemoveAllWhen 16 | import org.hexworks.cobalt.databinding.api.event.SetRetainAll 17 | import org.hexworks.cobalt.databinding.api.extension.toProperty 18 | import org.hexworks.cobalt.databinding.api.value.ValueValidationResult 19 | import org.hexworks.cobalt.databinding.internal.exception.CircularBindingException 20 | import org.hexworks.cobalt.databinding.internal.extensions.asInternalProperty 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | class SetBinding( 24 | source: ObservableSet, 25 | converter: (S) -> T 26 | ) : BaseBinding, PersistentSet>( 27 | source = source, 28 | target = persistentSetOf().toProperty().asInternalProperty().apply { 29 | this.transformValue { it.addAll(source.map(converter)) } 30 | }, 31 | subscriptions = mutableListOf() 32 | ) { 33 | 34 | override val name: String = "SetBinding" 35 | 36 | init { 37 | subscriptions.add( 38 | source.onChange { event -> 39 | if (event.trace.any { it.emitter == this }) { 40 | throw CircularBindingException( 41 | "Circular binding detected with trace ${event.trace.joinToString()} for property $this. Loop was prevented." 42 | ) 43 | } 44 | val type = event.type 45 | val oldValue = target.value 46 | val maybeNewValue: ValueValidationResult> = target.transformValue { 47 | // This is not nice...but the thing is that in order to make ObservableList only emit ListChanges 48 | // in a type safe way we'd have to add it as a type parameter to the whole property abstraction 49 | // ...which would also break the API 50 | (type as? SetChange)?.let { 51 | when (type) { 52 | is SetAdd<*> -> oldValue.add(converter(type.element as S)) 53 | is SetRemove<*> -> oldValue.remove(converter(type.element as S)) 54 | is SetAddAll<*> -> oldValue.addAll(type.elements.map { converter(it as S) }) 55 | is SetRemoveAll<*> -> oldValue.removeAll(type.elements.map { converter(it as S) }) 56 | is SetRemoveAllWhen<*> -> oldValue.removeAll { 57 | (type.predicate as (T) -> Boolean)(it) 58 | } 59 | is SetRetainAll<*> -> oldValue.retainAll(type.elements.map { converter(it as S) }) 60 | SetClear -> oldValue.clear() 61 | is SetPropertyChange<*> -> oldValue 62 | } 63 | } ?: oldValue 64 | } 65 | if (maybeNewValue.successful && oldValue != maybeNewValue.value) { 66 | Cobalt.eventbus.publish( 67 | event = ObservableValueChanged( 68 | oldValue = oldValue, 69 | newValue = maybeNewValue.value, 70 | observableValue = this, 71 | emitter = this, 72 | type = event.type, 73 | trace = listOf(event) + event.trace 74 | ), 75 | eventScope = propertyScope 76 | ) 77 | } 78 | } 79 | ) 80 | } 81 | 82 | override fun toString() = "${this::class.simpleName}(source=$source, target=$target)" 83 | } 84 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/binding/UnidirectionalBinding.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.Cobalt 4 | import org.hexworks.cobalt.databinding.api.binding.Binding 5 | import org.hexworks.cobalt.databinding.api.converter.Converter 6 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 7 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 8 | import org.hexworks.cobalt.databinding.internal.extensions.runWithDisposeOnFailure 9 | import org.hexworks.cobalt.databinding.internal.property.InternalProperty 10 | 11 | /** 12 | * Creates a **unidirectional** [Binding] between [source] and [target] which means that 13 | * whenever [source] gets updated [target] will be updated as well with the new value 14 | * but not the other way around. 15 | * [converter] will be used to convert the values between [source] and [target]. 16 | */ 17 | internal class UnidirectionalBinding( 18 | source: ObservableValue, 19 | target: InternalProperty, 20 | converter: Converter 21 | ) : BaseBinding( 22 | source = source, 23 | target = target, 24 | subscriptions = mutableListOf() 25 | ) { 26 | 27 | override val name: String = "UnidirectionalBinding" 28 | 29 | init { 30 | subscriptions.add( 31 | source.onChange { event -> 32 | runWithDisposeOnFailure { 33 | val oldValue = converter.convert(event.oldValue) 34 | val newValue = converter.convert(event.newValue) 35 | if (target.updateWithEvent( 36 | oldValue = oldValue, 37 | newValue = newValue, 38 | event = event 39 | ) 40 | ) { 41 | Cobalt.eventbus.publish( 42 | event = ObservableValueChanged( 43 | oldValue = oldValue, 44 | newValue = newValue, 45 | observableValue = this, 46 | emitter = this, 47 | trace = listOf(event) + event.trace, 48 | type = event.type 49 | ), 50 | eventScope = propertyScope 51 | ) 52 | } 53 | } 54 | } 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultListProperty.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.PersistentList 6 | import org.hexworks.cobalt.databinding.api.collection.ListProperty 7 | import org.hexworks.cobalt.databinding.api.event.* 8 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 9 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 10 | 11 | @Suppress("UNCHECKED_CAST") 12 | internal class DefaultListProperty( 13 | initialValue: PersistentList, 14 | optionalName: String?, 15 | validator: PropertyValidator> 16 | ) : BaseProperty>( 17 | initialValue = initialValue, 18 | name = optionalName ?: "DefaultListProperty", 19 | validator = validator 20 | ), 21 | ListProperty { 22 | 23 | override val size: Int 24 | get() = value.size 25 | 26 | override fun contains(element: T) = value.contains(element) 27 | 28 | override fun containsAll(elements: Collection) = value.containsAll(elements) 29 | 30 | override fun get(index: Int) = value[index] 31 | 32 | override fun indexOf(element: T) = value.indexOf(element) 33 | 34 | override fun isEmpty() = value.isEmpty() 35 | 36 | override fun iterator() = value.iterator() 37 | 38 | override fun lastIndexOf(element: T) = value.lastIndexOf(element) 39 | 40 | override fun listIterator() = value.listIterator() 41 | 42 | override fun listIterator(index: Int) = value.listIterator(index) 43 | 44 | override fun subList(fromIndex: Int, toIndex: Int) = value.subList(fromIndex, toIndex) 45 | 46 | override fun builder() = value.builder() 47 | 48 | // MUTATORS 49 | override fun add(element: T): PersistentList { 50 | return updateCurrentValue(ListAdd(element)) { it.add(element) } 51 | } 52 | 53 | override fun add(index: Int, element: T): PersistentList { 54 | return updateCurrentValue(ListAddAt(index, element)) { it.add(index, element) } 55 | } 56 | 57 | override fun set(index: Int, element: T): PersistentList { 58 | return updateCurrentValue(ListSet(index, element)) { it.set(index, element) } 59 | } 60 | 61 | override fun remove(element: T): PersistentList { 62 | return updateCurrentValue(ListRemove(element)) { it.remove(element) } 63 | } 64 | 65 | override fun removeAt(index: Int): PersistentList { 66 | return updateCurrentValue(ListRemoveAt(index)) { it.removeAt(index) } 67 | } 68 | 69 | override fun addAll(elements: Collection): PersistentList { 70 | return updateCurrentValue(ListAddAll(elements)) { it.addAll(elements) } 71 | } 72 | 73 | override fun addAll(index: Int, c: Collection): PersistentList { 74 | return updateCurrentValue(ListAddAllAt(index, c)) { it.addAll(index, c) } 75 | } 76 | 77 | override fun removeAll(elements: Collection): PersistentList { 78 | return updateCurrentValue(ListRemoveAll(elements)) { it.removeAll(elements) } 79 | } 80 | 81 | override fun removeAll(predicate: (T) -> Boolean): PersistentList { 82 | return updateCurrentValue(ListRemoveAllWhen(predicate)) { it.removeAll(predicate) } 83 | } 84 | 85 | override fun retainAll(elements: Collection): PersistentList { 86 | return updateCurrentValue(ListRetainAll(elements)) { it.retainAll(elements) } 87 | } 88 | 89 | override fun clear(): PersistentList { 90 | return updateCurrentValue(ListClear) { it.clear() } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultMapProperty.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.ImmutableCollection 6 | import kotlinx.collections.immutable.ImmutableSet 7 | import kotlinx.collections.immutable.PersistentMap 8 | import org.hexworks.cobalt.databinding.api.collection.MapProperty 9 | import org.hexworks.cobalt.databinding.api.event.* 10 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 11 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | class DefaultMapProperty( 15 | initialValue: PersistentMap, 16 | optionalName: String?, 17 | validator: PropertyValidator> 18 | ) : BaseProperty>( 19 | initialValue = initialValue, 20 | name = optionalName ?: "DefaultMapProperty", 21 | validator = validator 22 | ), MapProperty { 23 | 24 | override val entries: ImmutableSet> 25 | get() = value.entries 26 | 27 | override val keys: ImmutableSet 28 | get() = value.keys 29 | 30 | override val size: Int 31 | get() = value.size 32 | 33 | override val values: ImmutableCollection 34 | get() = value.values 35 | 36 | override fun containsKey(key: K) = value.containsKey(key) 37 | 38 | override fun containsValue(value: V) = this.value.containsValue(value) 39 | 40 | override fun get(key: K): V? = value[key] 41 | 42 | override fun isEmpty() = value.isEmpty() 43 | 44 | override fun builder() = value.builder() 45 | 46 | override fun put(key: K, value: V): PersistentMap { 47 | return updateCurrentValue(MapPut(key, value)) { it.put(key, value) } 48 | } 49 | 50 | override fun putAll(m: Map): PersistentMap { 51 | return updateCurrentValue(MapPutAll(m)) { it.putAll(m) } 52 | } 53 | 54 | override fun remove(key: K): PersistentMap { 55 | return updateCurrentValue(MapRemove(key)) { it.remove(key) } 56 | } 57 | 58 | override fun remove(key: K, value: V): PersistentMap { 59 | return updateCurrentValue(MapRemoveWithValue(key, value)) { it.remove(key, value) } 60 | } 61 | 62 | override fun clear(): PersistentMap { 63 | return updateCurrentValue(MapClear) { it.clear() } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultPropertyListProperty.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.PersistentList 6 | import org.hexworks.cobalt.core.api.UUID 7 | import org.hexworks.cobalt.core.api.extensions.identity 8 | import org.hexworks.cobalt.databinding.api.collection.ListProperty 9 | import org.hexworks.cobalt.databinding.api.event.* 10 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 11 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 12 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 13 | import org.hexworks.cobalt.events.api.Subscription 14 | 15 | @Suppress("UNCHECKED_CAST") 16 | internal class DefaultPropertyListProperty>( 17 | initialValue: PersistentList

, 18 | optionalName: String?, 19 | validator: PropertyValidator> 20 | ) : BaseProperty>( 21 | initialValue = initialValue, 22 | name = optionalName ?: "DefaultPropertyListProperty", 23 | validator = validator 24 | ), 25 | ListProperty

, 26 | List

{ 27 | 28 | private val uniqueProperties = mutableMapOf>() 29 | 30 | override val size: Int 31 | get() = value.size 32 | 33 | init { 34 | value.forEach { 35 | it.subscribeToChanges() 36 | } 37 | } 38 | 39 | override fun contains(element: P) = value.contains(element) 40 | 41 | override fun containsAll(elements: Collection

) = value.containsAll(elements) 42 | 43 | override fun isEmpty() = value.isEmpty() 44 | 45 | override fun iterator() = value.iterator() 46 | 47 | override fun get(index: Int) = value[index] 48 | 49 | override fun indexOf(element: P) = value.indexOf(element) 50 | 51 | override fun lastIndexOf(element: P) = value.lastIndexOf(element) 52 | 53 | override fun listIterator() = value.listIterator() 54 | 55 | override fun listIterator(index: Int) = value.listIterator(index) 56 | 57 | override fun builder(): PersistentList.Builder

= value.builder() 58 | 59 | // MUTATORS 60 | 61 | override fun add(element: P): PersistentList

{ 62 | element.subscribeToChanges() 63 | return updateCurrentValue(ListAdd(element)) { it.add(element) } 64 | } 65 | 66 | 67 | override fun add(index: Int, element: P): PersistentList

{ 68 | element.subscribeToChanges() 69 | return updateCurrentValue(ListAddAt(index, element)) { it.add(index, element) } 70 | } 71 | 72 | 73 | override fun addAll(elements: Collection

): PersistentList

{ 74 | elements.forEach { it.subscribeToChanges() } 75 | return updateCurrentValue(ListAddAll(elements)) { it.addAll(elements) } 76 | } 77 | 78 | 79 | override fun addAll(index: Int, c: Collection

): PersistentList

{ 80 | c.forEach { it.subscribeToChanges() } 81 | return updateCurrentValue(ListAddAllAt(index, c)) { it.addAll(index, c) } 82 | } 83 | 84 | 85 | override fun set(index: Int, element: P): PersistentList

{ 86 | if (value.size > index) { 87 | get(index).unsubscribeFromChanges() 88 | element.subscribeToChanges() 89 | } 90 | return updateCurrentValue(ListSet(index, element)) { it.set(index, element) } 91 | } 92 | 93 | 94 | override fun remove(element: P): PersistentList

{ 95 | element.unsubscribeFromChanges() 96 | return updateCurrentValue(ListRemove(element)) { it.remove(element) } 97 | } 98 | 99 | 100 | override fun removeAt(index: Int): PersistentList

{ 101 | if (value.size > index) { 102 | value[index].unsubscribeFromChanges() 103 | } 104 | return updateCurrentValue(ListRemoveAt(index)) { it.removeAt(index) } 105 | } 106 | 107 | 108 | override fun removeAll(elements: Collection

): PersistentList

{ 109 | elements.forEach { it.unsubscribeFromChanges() } 110 | return updateCurrentValue(ListRemoveAll(elements)) { it.removeAll(elements) } 111 | } 112 | 113 | 114 | override fun removeAll(predicate: (P) -> Boolean): PersistentList

{ 115 | value.filter(predicate).forEach { it.unsubscribeFromChanges() } 116 | return updateCurrentValue(ListRemoveAllWhen(predicate)) { it.removeAll(predicate) } 117 | } 118 | 119 | 120 | override fun retainAll(elements: Collection

): PersistentList

{ 121 | value.minus(elements).forEach { it.unsubscribeFromChanges() } 122 | return updateCurrentValue(ListRetainAll(elements)) { it.retainAll(elements) } 123 | } 124 | 125 | 126 | override fun clear(): PersistentList

{ 127 | value.forEach { it.unsubscribeFromChanges() } 128 | if (uniqueProperties.isNotEmpty()) { 129 | logger.warn { 130 | """ 131 | There are still remaining property subscriptions after clearing the list. 132 | If you see this it means that there is a bug. Please contact the developers 133 | """ 134 | } 135 | } 136 | return updateCurrentValue(ListClear) { it.clear() } 137 | } 138 | 139 | private fun P.subscribeToChanges() { 140 | val p = this 141 | if (uniqueProperties.containsKey(p.id).not()) { 142 | uniqueProperties[p.id] = this to onChange { change -> 143 | updateCurrentValue(ListPropertyChange(change), identity()) 144 | } 145 | } 146 | } 147 | 148 | private fun P.unsubscribeFromChanges() { 149 | val p = this 150 | uniqueProperties.remove(p.id)?.second?.dispose() 151 | } 152 | 153 | override fun equals(other: Any?): Boolean { 154 | if (this === other) return true 155 | if (other == null || this::class != other::class) return false 156 | if (!super.equals(other)) return false 157 | 158 | other as DefaultPropertyListProperty<*, *> 159 | 160 | if (value.map { it.value } != other.value.map { it.value }) return false 161 | 162 | return true 163 | } 164 | 165 | override fun hashCode(): Int { 166 | var result = super.hashCode() 167 | result = 31 * result + value.hashCode() 168 | return result 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultPropertyMapProperty.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.ImmutableCollection 6 | import kotlinx.collections.immutable.ImmutableSet 7 | import kotlinx.collections.immutable.PersistentMap 8 | import org.hexworks.cobalt.core.api.UUID 9 | import org.hexworks.cobalt.core.api.extensions.identity 10 | import org.hexworks.cobalt.databinding.api.collection.MapProperty 11 | import org.hexworks.cobalt.databinding.api.event.ListPropertyChange 12 | import org.hexworks.cobalt.databinding.api.event.MapClear 13 | import org.hexworks.cobalt.databinding.api.event.MapPut 14 | import org.hexworks.cobalt.databinding.api.event.MapPutAll 15 | import org.hexworks.cobalt.databinding.api.event.MapRemove 16 | import org.hexworks.cobalt.databinding.api.event.MapRemoveWithValue 17 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 18 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 19 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 20 | import org.hexworks.cobalt.events.api.Subscription 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | internal class DefaultPropertyMapProperty>( 24 | initialValue: PersistentMap, 25 | optionalName: String?, 26 | validator: PropertyValidator> 27 | ) : BaseProperty>( 28 | initialValue = initialValue, 29 | name = optionalName ?: "DefaultPropertyMapProperty", 30 | validator = validator 31 | ), 32 | MapProperty { 33 | 34 | private val uniqueProperties = mutableMapOf>() 35 | 36 | override val size: Int 37 | get() = value.size 38 | 39 | init { 40 | value.values.forEach { 41 | it.subscribeToChanges() 42 | } 43 | } 44 | 45 | override fun containsKey(key: K) = value.containsKey(key) 46 | 47 | override fun containsValue(value: P) = this.value.containsValue(value) 48 | 49 | override fun get(key: K): P? = value.get(key) 50 | 51 | override fun isEmpty() = value.isEmpty() 52 | 53 | override val entries: ImmutableSet> 54 | get() = value.entries 55 | override val keys: ImmutableSet 56 | get() = value.keys 57 | override val values: ImmutableCollection

58 | get() = value.values 59 | 60 | override fun builder() = value.builder() 61 | 62 | override fun put(key: K, value: P): PersistentMap { 63 | value.subscribeToChanges() 64 | return updateCurrentValue(MapPut(key, value)) { it.put(key, value) } 65 | } 66 | 67 | override fun putAll(m: Map): PersistentMap { 68 | m.values.forEach { it.subscribeToChanges() } 69 | return updateCurrentValue(MapPutAll(m)) { it.putAll(m) } 70 | } 71 | 72 | override fun remove(key: K): PersistentMap { 73 | value[key]?.unsubscribeFromChanges() 74 | return updateCurrentValue(MapRemove(key)) { it.remove(key) } 75 | } 76 | 77 | override fun remove(key: K, value: P): PersistentMap { 78 | this.value[key]?.unsubscribeFromChanges() 79 | return updateCurrentValue(MapRemoveWithValue(key, value)) { it.remove(key, value) } 80 | } 81 | 82 | override fun clear(): PersistentMap { 83 | value.values.forEach { it.unsubscribeFromChanges() } 84 | return updateCurrentValue(MapClear) { it.clear() } 85 | } 86 | 87 | private fun P.subscribeToChanges() { 88 | val p = this 89 | if (uniqueProperties.containsKey(p.id).not()) { 90 | uniqueProperties[p.id] = this to onChange { ovc -> 91 | updateCurrentValue(ListPropertyChange(ovc), identity()) 92 | } 93 | } 94 | } 95 | 96 | private fun P.unsubscribeFromChanges() { 97 | val p = this 98 | uniqueProperties.remove(p.id)?.second?.dispose() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultPropertySetProperty.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.PersistentSet 6 | import org.hexworks.cobalt.core.api.UUID 7 | import org.hexworks.cobalt.core.api.extensions.identity 8 | import org.hexworks.cobalt.databinding.api.collection.SetProperty 9 | import org.hexworks.cobalt.databinding.api.event.* 10 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 11 | import org.hexworks.cobalt.databinding.api.value.ObservableValue 12 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 13 | import org.hexworks.cobalt.events.api.Subscription 14 | 15 | @Suppress("UNCHECKED_CAST") 16 | internal class DefaultPropertySetProperty>( 17 | initialValue: PersistentSet, 18 | optionalName: String?, 19 | validator: PropertyValidator> 20 | ) : BaseProperty>( 21 | initialValue = initialValue, 22 | name = optionalName ?: "DefaultPropertySetProperty", 23 | validator = validator 24 | ), 25 | SetProperty { 26 | 27 | private val uniqueProperties = mutableMapOf>() 28 | 29 | override val size: Int 30 | get() = value.size 31 | 32 | init { 33 | value.forEach { 34 | it.subscribeToChanges() 35 | } 36 | } 37 | 38 | override fun contains(element: V) = value.contains(element) 39 | 40 | override fun containsAll(elements: Collection) = value.containsAll(elements) 41 | 42 | override fun isEmpty() = value.isEmpty() 43 | 44 | override fun iterator() = value.iterator() 45 | 46 | override fun builder() = value.builder() 47 | 48 | override fun add(element: V): PersistentSet { 49 | element.subscribeToChanges() 50 | return updateCurrentValue(SetAdd(element)) { it.add(element) } 51 | } 52 | 53 | override fun addAll(elements: Collection): PersistentSet { 54 | elements.forEach { it.subscribeToChanges() } 55 | return updateCurrentValue(SetAddAll(elements)) { it.addAll(elements) } 56 | } 57 | 58 | override fun remove(element: V): PersistentSet { 59 | element.unsubscribeFromChanges() 60 | return updateCurrentValue(SetRemove(element)) { it.remove(element) } 61 | } 62 | 63 | override fun removeAll(predicate: (V) -> Boolean): PersistentSet { 64 | value.filter(predicate).forEach { it.unsubscribeFromChanges() } 65 | return updateCurrentValue(SetRemoveAllWhen(predicate)) { it.removeAll(predicate) } 66 | } 67 | 68 | override fun removeAll(elements: Collection): PersistentSet { 69 | elements.forEach { it.unsubscribeFromChanges() } 70 | return updateCurrentValue(SetRemoveAll(elements)) { it.removeAll(elements) } 71 | } 72 | 73 | override fun retainAll(elements: Collection): PersistentSet { 74 | value.minus(elements).forEach { it.unsubscribeFromChanges() } 75 | return updateCurrentValue(SetRetainAll(elements)) { it.retainAll(elements) } 76 | } 77 | 78 | override fun clear(): PersistentSet { 79 | value.forEach { it.unsubscribeFromChanges() } 80 | if (uniqueProperties.isNotEmpty()) { 81 | logger.warn { 82 | """ 83 | There are still remaining property subscriptions after clearing the set. 84 | If you see this it means that there is a bug. Please contact the developers 85 | """ 86 | } 87 | } 88 | return updateCurrentValue(SetClear) { it.clear() } 89 | } 90 | 91 | private fun V.subscribeToChanges() { 92 | val v = this 93 | if (uniqueProperties.containsKey(v.id).not()) { 94 | uniqueProperties[v.id] = this to onChange { ovc -> 95 | updateCurrentValue(SetPropertyChange(ovc), identity()) 96 | } 97 | } 98 | } 99 | 100 | private fun V.unsubscribeFromChanges() { 101 | val v = this 102 | uniqueProperties.remove(v.id)?.second?.dispose() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultSetProperty.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.PersistentSet 6 | import org.hexworks.cobalt.databinding.api.collection.SetProperty 7 | import org.hexworks.cobalt.databinding.api.event.* 8 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 9 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 10 | 11 | @Suppress("UNCHECKED_CAST") 12 | class DefaultSetProperty( 13 | initialValue: PersistentSet, 14 | optionalName: String?, 15 | validator: PropertyValidator> 16 | ) : BaseProperty>( 17 | initialValue = initialValue, 18 | name = optionalName ?: "DefaultSetProperty", 19 | validator = validator 20 | ), SetProperty { 21 | 22 | override val size: Int 23 | get() = value.size 24 | 25 | override fun contains(element: T) = value.contains(element) 26 | 27 | override fun containsAll(elements: Collection) = value.containsAll(elements) 28 | 29 | override fun isEmpty() = value.isEmpty() 30 | 31 | override fun iterator() = value.iterator() 32 | 33 | override fun builder() = value.builder() 34 | 35 | override fun add(element: T): PersistentSet { 36 | return updateCurrentValue(SetAdd(element)) { it.add(element) } 37 | } 38 | 39 | override fun addAll(elements: Collection): PersistentSet { 40 | return updateCurrentValue(SetAddAll(elements)) { it.addAll(elements) } 41 | } 42 | 43 | override fun remove(element: T): PersistentSet { 44 | return updateCurrentValue(SetRemove(element)) { it.remove(element) } 45 | } 46 | 47 | override fun removeAll(predicate: (T) -> Boolean): PersistentSet { 48 | return updateCurrentValue(SetRemoveAllWhen(predicate)) { it.removeAll(predicate) } 49 | } 50 | 51 | override fun removeAll(elements: Collection): PersistentSet { 52 | return updateCurrentValue(SetRemoveAll(elements)) { it.removeAll(elements) } 53 | } 54 | 55 | override fun retainAll(elements: Collection): PersistentSet { 56 | return updateCurrentValue(SetRetainAll(elements)) { it.retainAll(elements) } 57 | } 58 | 59 | override fun clear(): PersistentSet { 60 | return updateCurrentValue(SetClear) { it.clear() } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/ListBindingDecorator.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.PersistentList 6 | import org.hexworks.cobalt.core.api.UUID 7 | import org.hexworks.cobalt.core.api.behavior.DisposeState 8 | import org.hexworks.cobalt.databinding.api.binding.Binding 9 | import org.hexworks.cobalt.databinding.api.collection.ObservableListBinding 10 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 11 | import org.hexworks.cobalt.events.api.Subscription 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | internal class ListBindingDecorator( 15 | private val binding: Binding>, 16 | optionalName: String? = null, 17 | ) : ObservableListBinding { 18 | 19 | override val id: UUID 20 | get() = binding.id 21 | 22 | override val value: PersistentList 23 | get() = binding.value 24 | 25 | override val size: Int 26 | get() = value.size 27 | 28 | override val name = optionalName ?: "ListBindingDecorator" 29 | 30 | override fun onChange(fn: (ObservableValueChanged>) -> Unit): Subscription { 31 | return binding.onChange(fn) 32 | } 33 | 34 | override operator fun contains(element: T) = value.contains(element) 35 | 36 | override fun containsAll(elements: Collection) = value.containsAll(elements) 37 | 38 | override operator fun get(index: Int) = value[index] 39 | 40 | override fun indexOf(element: T) = value.indexOf(element) 41 | 42 | override fun isEmpty() = value.isEmpty() 43 | 44 | override fun iterator() = value.iterator() 45 | 46 | override fun lastIndexOf(element: T) = value.lastIndexOf(element) 47 | 48 | override fun listIterator() = value.listIterator() 49 | 50 | override fun listIterator(index: Int) = value.listIterator(index) 51 | 52 | override fun subList(fromIndex: Int, toIndex: Int) = value.subList(fromIndex, toIndex) 53 | 54 | override fun add(element: T): PersistentList { 55 | return value.add(element) 56 | } 57 | 58 | override fun remove(element: T): PersistentList { 59 | return value.remove(element) 60 | } 61 | 62 | override fun addAll(elements: Collection): PersistentList { 63 | return value.addAll(elements) 64 | } 65 | 66 | override fun addAll(index: Int, c: Collection): PersistentList { 67 | return value.addAll(index, c) 68 | } 69 | 70 | override fun removeAll(elements: Collection): PersistentList { 71 | return value.removeAll(elements) 72 | } 73 | 74 | override fun removeAll(predicate: (T) -> Boolean): PersistentList { 75 | return value.removeAll(predicate) 76 | } 77 | 78 | override fun retainAll(elements: Collection): PersistentList { 79 | return value.retainAll(elements) 80 | } 81 | 82 | override fun clear(): PersistentList { 83 | return value.clear() 84 | } 85 | 86 | override fun set(index: Int, element: T): PersistentList { 87 | return value.set(index, element) 88 | } 89 | 90 | override fun add(index: Int, element: T): PersistentList { 91 | return value.add(index, element) 92 | } 93 | 94 | override fun removeAt(index: Int): PersistentList { 95 | return value.removeAt(index) 96 | } 97 | 98 | override fun builder() = value.builder() 99 | 100 | override val disposeState: DisposeState 101 | get() = binding.disposeState 102 | 103 | override fun dispose(disposeState: DisposeState) = binding.dispose(disposeState) 104 | } 105 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/collections/SetBindingDecorator.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_PARAMETER") 2 | 3 | package org.hexworks.cobalt.databinding.internal.collections 4 | 5 | import kotlinx.collections.immutable.PersistentSet 6 | import org.hexworks.cobalt.core.api.UUID 7 | import org.hexworks.cobalt.core.api.behavior.DisposeState 8 | import org.hexworks.cobalt.databinding.api.binding.Binding 9 | import org.hexworks.cobalt.databinding.api.collection.ObservableSetBinding 10 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 11 | import org.hexworks.cobalt.events.api.Subscription 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | internal class SetBindingDecorator( 15 | private val binding: Binding>, 16 | optionalName: String? = null, 17 | ) : ObservableSetBinding { 18 | 19 | override val id: UUID 20 | get() = binding.id 21 | 22 | override val value: PersistentSet 23 | get() = binding.value 24 | 25 | override val size: Int 26 | get() = value.size 27 | 28 | override val name = optionalName ?: "SetBindingDecorator" 29 | 30 | override fun onChange(fn: (ObservableValueChanged>) -> Unit): Subscription { 31 | return binding.onChange(fn) 32 | } 33 | 34 | override fun contains(element: T) = value.contains(element) 35 | 36 | override fun containsAll(elements: Collection) = value.containsAll(elements) 37 | 38 | override fun isEmpty() = value.isEmpty() 39 | 40 | override fun iterator() = value.iterator() 41 | 42 | override fun add(element: T): PersistentSet { 43 | return value.add(element) 44 | } 45 | 46 | override fun remove(element: T): PersistentSet { 47 | return value.remove(element) 48 | } 49 | 50 | override fun addAll(elements: Collection): PersistentSet { 51 | return value.addAll(elements) 52 | } 53 | 54 | override fun removeAll(elements: Collection): PersistentSet { 55 | return value.removeAll(elements) 56 | } 57 | 58 | override fun removeAll(predicate: (T) -> Boolean): PersistentSet { 59 | return value.removeAll(predicate) 60 | } 61 | 62 | override fun retainAll(elements: Collection): PersistentSet { 63 | return value.retainAll(elements) 64 | } 65 | 66 | override fun clear(): PersistentSet { 67 | return value.clear() 68 | } 69 | 70 | override fun builder() = value.builder() 71 | 72 | override val disposeState: DisposeState 73 | get() = binding.disposeState 74 | 75 | override fun dispose(disposeState: DisposeState) = binding.dispose(disposeState) 76 | } 77 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/event/PropertyScope.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.event 2 | 3 | import org.hexworks.cobalt.core.api.UUID 4 | import org.hexworks.cobalt.events.api.EventScope 5 | 6 | /** 7 | * [EventScope] which can be used within property and binding objects. 8 | */ 9 | data class PropertyScope(val id: UUID) : EventScope 10 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/exception/CircularBindingException.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.exception 2 | 3 | internal class CircularBindingException(message: String) : RuntimeException(message) 4 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/extensions/BindingExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.extensions 2 | 3 | import org.hexworks.cobalt.databinding.api.binding.Binding 4 | import org.hexworks.cobalt.core.api.behavior.DisposedByException 5 | 6 | internal fun Binding.runWithDisposeOnFailure(fn: () -> Unit) { 7 | try { 8 | fn() 9 | } catch (e: Exception) { 10 | dispose(DisposedByException(e)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/extensions/PropertyExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.extensions 2 | 3 | import org.hexworks.cobalt.databinding.api.property.Property 4 | import org.hexworks.cobalt.databinding.internal.property.InternalProperty 5 | 6 | internal fun Property.asInternalProperty(): InternalProperty { 7 | return this as InternalProperty 8 | } 9 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/property/DefaultProperty.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.property 2 | 3 | import org.hexworks.cobalt.databinding.api.property.PropertyValidator 4 | import org.hexworks.cobalt.databinding.internal.property.base.BaseProperty 5 | 6 | @Suppress("UNCHECKED_CAST") 7 | internal class DefaultProperty( 8 | initialValue: T, 9 | optionalName: String?, 10 | validator: PropertyValidator = { _, _ -> true } 11 | ) : BaseProperty( 12 | initialValue = initialValue, 13 | name = optionalName ?: "DefaultProperty", 14 | validator = validator 15 | ) 16 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/property/DefaultPropertyDelegate.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.property 2 | 3 | import org.hexworks.cobalt.databinding.api.property.Property 4 | import org.hexworks.cobalt.databinding.api.property.PropertyDelegate 5 | import kotlin.reflect.KProperty 6 | 7 | internal class DefaultPropertyDelegate(private val property: Property) : PropertyDelegate, 8 | Property by property { 9 | 10 | override fun getValue(thisRef: Any?, property: KProperty<*>): T { 11 | return this.property.value 12 | } 13 | 14 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 15 | this.property.value = value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/databinding/internal/property/InternalProperty.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.property 2 | 3 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 4 | import org.hexworks.cobalt.databinding.api.property.Property 5 | import org.hexworks.cobalt.databinding.internal.event.PropertyScope 6 | import org.hexworks.cobalt.logging.api.Logger 7 | 8 | interface InternalProperty : Property { 9 | 10 | val logger: Logger 11 | val propertyScope: PropertyScope 12 | 13 | /** 14 | * Tries to update the [value] of this [InternalProperty] to [newValue] using [event] 15 | * to check for circular dependencies. This function is only for internal usage. 16 | * @return `true` if change happened, `false` if not 17 | */ 18 | fun updateWithEvent( 19 | oldValue: T, 20 | newValue: T, 21 | event: ObservableValueChanged<*> 22 | ): Boolean 23 | } 24 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/CallbackResult.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | 3 | sealed class CallbackResult 4 | 5 | object KeepSubscription : CallbackResult() 6 | 7 | object DisposeSubscription : CallbackResult() 8 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/Event.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | 3 | /** 4 | * Common interface for all [Event]s which can be sent using the [EventBus]. Each event 5 | * must have a [key] which can be used to group events from the same origin / cause together. 6 | * [trace] can be used to check the chain of events which caused this [Event]. Each [Event] 7 | * must also have an [emitter] which is the object responsible for emitting this [Event]. 8 | */ 9 | interface Event { 10 | 11 | /** 12 | * A unique key for this [Event]. 13 | * If not supplied, [kotlin.reflect.KClass.simpleName] will be used. 14 | */ 15 | val key: String 16 | get() = this::class.simpleName 17 | ?: throw IllegalArgumentException("Event class doesn't have a name: ${this::class}") 18 | 19 | /** 20 | * The object which emitted *this* [Event]. 21 | */ 22 | val emitter: Any 23 | 24 | /** 25 | * Contains a (possibly empty) sequence of [Event]s which lead up to *this* [Event] in reverse 26 | * chronological order (most recent is first, oldest is last). 27 | */ 28 | val trace: Iterable 29 | get() = listOf() 30 | } 31 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/EventBus.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | 3 | import org.hexworks.cobalt.events.internal.ApplicationScope 4 | import org.hexworks.cobalt.events.internal.DefaultEventBus 5 | 6 | /** 7 | * An [EventBus] can be used to broadcast [Event]s to subscribers of that [Event]. 8 | */ 9 | interface EventBus { 10 | 11 | /** 12 | * Returns all subscribers of the event with the given [key] and [eventScope]. 13 | */ 14 | fun fetchSubscribersOf( 15 | eventScope: EventScope = ApplicationScope, 16 | key: String 17 | ): Iterable 18 | 19 | /** 20 | * Subscribes the callee to [Event]s which have [eventScope] and [key]. 21 | * [fn] will be called whenever there is a match. [CallbackResult] can 22 | * be used to control the [Subscription]: 23 | * - [KeepSubscription] will keep the [Subscription] 24 | * - [DisposeSubscription] will dispose it 25 | */ 26 | fun subscribeTo( 27 | eventScope: EventScope = ApplicationScope, 28 | key: String, 29 | fn: (T) -> CallbackResult 30 | ): Subscription 31 | 32 | /** 33 | * Publishes the given [Event] to all listeners who have the same 34 | * [eventScope] and [Event.key]. 35 | */ 36 | fun publish( 37 | event: Event, 38 | eventScope: EventScope = ApplicationScope 39 | ) 40 | 41 | /** 42 | * Cancels all [Subscription]s for the given [scope]. 43 | */ 44 | fun cancelScope(scope: EventScope) 45 | 46 | /** 47 | * Cancels all subscriptions and closes this [EventBus]. 48 | */ 49 | fun close() 50 | 51 | companion object { 52 | 53 | /** 54 | * Creates a new [EventBus]. 55 | */ 56 | fun create(): EventBus = DefaultEventBus() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/EventBusExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | 3 | import org.hexworks.cobalt.events.internal.ApplicationScope 4 | 5 | /** 6 | * Same as [subscribeTo], but will always [KeepSubscription] when 7 | * the callback is called. 8 | */ 9 | inline fun EventBus.simpleSubscribeTo( 10 | eventScope: EventScope = ApplicationScope, 11 | noinline callback: (T) -> Unit 12 | ): Subscription { 13 | val key = T::class.simpleName ?: throw IllegalArgumentException( 14 | "Event class doesn't have a name: ${T::class}" 15 | ) 16 | return subscribeTo( 17 | eventScope = eventScope, 18 | key = key, 19 | fn = { 20 | callback(it) 21 | KeepSubscription 22 | } 23 | ) 24 | } 25 | 26 | /** 27 | * Reified variant of [EventBus.simpleSubscribeTo]. 28 | */ 29 | inline fun EventBus.subscribeTo( 30 | eventScope: EventScope = ApplicationScope, 31 | noinline callback: (T) -> CallbackResult 32 | ): Subscription { 33 | val key = T::class.simpleName ?: throw IllegalArgumentException( 34 | "Event class doesn't have a name: ${T::class}" 35 | ) 36 | return subscribeTo( 37 | eventScope = eventScope, 38 | key = key, 39 | fn = callback 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/EventExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/EventScope.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | 3 | /** 4 | * Used to scope [Event]s. Each [EventScope] has a separate 5 | * namespace from other [EventScope]s and [Event]s sent with different 6 | * scopes are completely isolated from each other. 7 | * Tip: use `object`s to implement [EventScope] for ease of use. 8 | */ 9 | interface EventScope 10 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/api/Subscription.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.api 2 | 3 | import org.hexworks.cobalt.core.api.behavior.Disposable 4 | 5 | /** 6 | * Represents a subscription to an event. 7 | */ 8 | interface Subscription : Disposable 9 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/internal/ApplicationScope.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.internal 2 | 3 | import org.hexworks.cobalt.events.api.EventScope 4 | 5 | /** 6 | * Default [EventScope] which is used as a default in the [org.hexworks.cobalt.events.api.EventBus] 7 | * and is a suitable default scope for user-sent [org.hexworks.cobalt.events.api.Event]s. 8 | */ 9 | object ApplicationScope : EventScope 10 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/events/internal/DefaultEventBus.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events.internal 2 | 3 | import org.hexworks.cobalt.core.api.behavior.DisposeState 4 | import org.hexworks.cobalt.core.api.behavior.DisposedByException 5 | import org.hexworks.cobalt.core.api.behavior.NotDisposed 6 | import org.hexworks.cobalt.events.api.* 7 | import org.hexworks.cobalt.logging.api.LoggerFactory 8 | 9 | internal class DefaultEventBus : EventBus { 10 | 11 | private var closed = false 12 | 13 | private var subscriptions = mutableMapOf>>() 14 | private val logger = LoggerFactory.getLogger(this::class) 15 | 16 | override fun fetchSubscribersOf(eventScope: EventScope, key: String): Iterable { 17 | return subscriptions.getOrElse(SubscriberKey(eventScope, key)) { listOf() }.toList() 18 | } 19 | 20 | override fun subscribeTo( 21 | eventScope: EventScope, 22 | key: String, 23 | fn: (T) -> CallbackResult 24 | ): Subscription = whenNotClosed { 25 | try { 26 | logger.debug { "Subscribing to $key with scope $eventScope." } 27 | val subscription = EventBusSubscription( 28 | eventScope = eventScope, 29 | key = key, 30 | callback = fn 31 | ) 32 | val subKey = SubscriberKey(eventScope, key) 33 | val subs = subscriptions[subKey] ?: run { 34 | val list = mutableListOf>() 35 | subscriptions[subKey] = list 36 | list 37 | } 38 | subs.add(subscription) 39 | subscription 40 | } catch (e: Exception) { 41 | logger.warn(e) { "Failed to subscribe to event key $key with scope $eventScope" } 42 | throw e 43 | } 44 | } 45 | 46 | @Suppress("UNCHECKED_CAST") 47 | override fun publish( 48 | event: Event, 49 | eventScope: EventScope 50 | ): Unit = whenNotClosed { 51 | logger.debug { 52 | "Publishing event with key ${event.key} and scope $eventScope." 53 | } 54 | subscriptions[SubscriberKey(eventScope, event.key)]?.let { subscribers -> 55 | subscribers.toList().forEach { subscription: EventBusSubscription<*> -> 56 | try { 57 | if (subscription.callback.fixType().invoke(event) is DisposeSubscription) { 58 | subscription.dispose() 59 | } 60 | } catch (e: Exception) { 61 | logger.warn(e) { "Cancelling failed subscription $subscription." } 62 | try { 63 | subscription.dispose(DisposedByException(e)) 64 | } catch (e: Exception) { 65 | logger.warn(e) { "Failed to cancel subscription $subscription." } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | override fun cancelScope(scope: EventScope): Unit = whenNotClosed { 73 | logger.debug { "Cancelling scope $scope." } 74 | subscriptions.filter { it.key.scope == scope } 75 | .flatMap { it.value } 76 | .forEach { 77 | try { 78 | it.dispose() 79 | } catch (e: Exception) { 80 | logger.warn { "Cancelling subscription failed while cancelling scope. Reason: ${e.message}" } 81 | } 82 | } 83 | } 84 | 85 | override fun close() { 86 | closed = true 87 | subscriptions.values.flatten().forEach { it.dispose() } 88 | } 89 | 90 | private fun whenNotClosed(fn: () -> T): T { 91 | return if (closed) error("This Event Bus is already closed.") else fn() 92 | } 93 | 94 | private data class SubscriberKey(val scope: EventScope, val key: String) 95 | 96 | private inner class EventBusSubscription( 97 | val eventScope: EventScope, 98 | val key: String, 99 | val callback: (T) -> CallbackResult 100 | ) : Subscription { 101 | 102 | override var disposeState: DisposeState = NotDisposed 103 | private set 104 | 105 | @Suppress("UNCHECKED_CAST") 106 | override fun dispose(disposeState: DisposeState) { 107 | return try { 108 | logger.debug { 109 | "Cancelling event bus subscription with scope '$eventScope' and key '$key'." 110 | } 111 | val key = SubscriberKey(eventScope, key) 112 | this.disposeState = disposeState 113 | subscriptions[key]?.let { subs -> 114 | subs.remove(this) 115 | if (subs.isEmpty()) { 116 | subscriptions.remove(key) 117 | } 118 | } 119 | Unit 120 | } catch (e: Exception) { 121 | logger.warn(e) { "Cancelling event bus subscription failed." } 122 | throw e 123 | } 124 | } 125 | } 126 | 127 | @Suppress("UNCHECKED_CAST") 128 | private fun ((Nothing) -> CallbackResult).fixType() = this as (Event) -> CallbackResult 129 | } 130 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/logging/api/LogException.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.logging.api 2 | 3 | class LogException(override val message: String, val other: Throwable?) : Exception(message, other) { 4 | override fun toString() = "$message : $other" 5 | } -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/logging/api/Logger.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.logging.api 2 | 3 | /** 4 | * The [Logger] interface contains all possible logging operations. 5 | */ 6 | interface Logger { 7 | 8 | /** 9 | * The name of this logger instance 10 | */ 11 | val name: String 12 | 13 | /** 14 | * Lazily log an exception (throwable) at the TRACE level with an 15 | * accompanying message. 16 | */ 17 | fun trace(t: Throwable? = null, msgFn: () -> String) 18 | 19 | /** 20 | * Lazily log an exception (throwable) at the DEBUG level with an 21 | * accompanying message. 22 | */ 23 | fun debug(t: Throwable? = null, msgFn: () -> String) 24 | 25 | /** 26 | * Lazily log an exception (throwable) at the INFO level with an 27 | * accompanying message. 28 | */ 29 | fun info(t: Throwable? = null, msgFn: () -> String) 30 | 31 | /** 32 | * Lazily log an exception (throwable) at the WARN level with an 33 | * accompanying message. 34 | */ 35 | fun warn(t: Throwable? = null, msgFn: () -> String) 36 | 37 | /** 38 | * Lazily log an exception (throwable) at the ERROR level with an 39 | * accompanying message. 40 | */ 41 | fun error(t: Throwable? = null, msgFn: () -> String) 42 | } 43 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/logging/api/LoggerFactory.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.logging.api 2 | 3 | import org.hexworks.cobalt.logging.internal.DefaultLogger 4 | import kotlin.reflect.KClass 5 | 6 | object LoggerFactory { 7 | 8 | fun getLogger(name: String): Logger = DefaultLogger(name) 9 | 10 | fun getLogger(kClass: KClass): Logger = 11 | getLogger(kClass.simpleName ?: "name-is-missing-try-using-the-string-overload") 12 | } 13 | -------------------------------------------------------------------------------- /cobalt.core/src/commonMain/kotlin/org/hexworks/cobalt/logging/internal/DefaultLogger.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.logging.internal 2 | 3 | import org.hexworks.cobalt.logging.api.LogException 4 | import org.hexworks.cobalt.logging.api.Logger 5 | 6 | internal class DefaultLogger(override val name: String) : Logger { 7 | 8 | private val logger = korlibs.logger.Logger(name) 9 | 10 | override fun trace(t: Throwable?, msgFn: () -> String) { 11 | if (t == null) logger.trace(msgFn) else logger.trace { LogException(msgFn(), t) } 12 | } 13 | 14 | override fun debug(t: Throwable?, msgFn: () -> String) { 15 | if (t == null) logger.debug(msgFn) else logger.debug { LogException(msgFn(), t) } 16 | } 17 | 18 | override fun info(t: Throwable?, msgFn: () -> String) { 19 | if (t == null) logger.info(msgFn) else logger.info { LogException(msgFn(), t) } 20 | } 21 | 22 | override fun warn(t: Throwable?, msgFn: () -> String) { 23 | if (t == null) logger.warn(msgFn) else logger.warn { LogException(msgFn(), t) } 24 | } 25 | 26 | override fun error(t: Throwable?, msgFn: () -> String) { 27 | if (t == null) logger.error(msgFn) else logger.error { LogException(msgFn(), t) } 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/core/internal/AtomTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.core.internal 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class AtomTest { 7 | 8 | @Test 9 | fun test() { 10 | val ref = 1.toAtom() 11 | 12 | ref.transform { 13 | 2 14 | } 15 | assertEquals(2, ref.get()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/binding/BidirectionalConverterBindingTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.core.api.behavior.DisposedByException 4 | import org.hexworks.cobalt.databinding.api.binding.Binding 5 | import org.hexworks.cobalt.databinding.api.converter.IsomorphicConverter 6 | import org.hexworks.cobalt.databinding.api.extension.toProperty 7 | import org.hexworks.cobalt.databinding.api.property.Property 8 | import kotlin.test.BeforeTest 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | @Suppress("TestFunctionName", "FunctionName") 13 | class BidirectionalConverterBindingTest { 14 | 15 | lateinit var target: Property 16 | 17 | @BeforeTest 18 | fun Set_up() { 19 | target = ONE.toProperty() 20 | } 21 | 22 | @Test 23 | fun When_target_property_is_bound_bidirectionally_to_other_property_with_different_type_its_value_should_be_updated() { 24 | val otherProperty = 2.toProperty() 25 | 26 | bindTargetToOther(otherProperty) 27 | 28 | assertEquals(expected = TWO, actual = target.value) 29 | } 30 | 31 | @Test 32 | fun When_target_property_is_bound_bidirectionally_to_other_property_and_other_property_is_updated_target_should_get_updated() { 33 | val otherProperty = 2.toProperty() 34 | 35 | bindTargetToOther(otherProperty) 36 | 37 | otherProperty.value = 1 38 | 39 | assertEquals(expected = ONE, actual = target.value) 40 | } 41 | 42 | @Test 43 | fun When_target_property_is_bound_bidirectionally_to_other_property_and_target_property_is_updated_other_should_get_updated() { 44 | val otherProperty = 2.toProperty() 45 | 46 | bindTargetToOther(otherProperty) 47 | 48 | target.value = ONE 49 | 50 | assertEquals(expected = 1, actual = otherProperty.value) 51 | } 52 | 53 | @Test 54 | fun When_target_property_is_bound_bidirectionally_to_other_property_and_target_property_is_updated_with_exception_binding_should_be_disposed() { 55 | val otherProperty = 2.toProperty() 56 | 57 | val binding = bindTargetToOther(otherProperty) 58 | 59 | target.value = FOO 60 | 61 | assertEquals( 62 | expected = DisposedByException::class, 63 | actual = binding.disposeState::class, 64 | message = "Binding should have been disposed because of exception" 65 | ) 66 | } 67 | 68 | private fun bindTargetToOther(otherProperty: Property): Binding { 69 | return target.bind( 70 | other = otherProperty, 71 | converter = object : IsomorphicConverter { 72 | 73 | override fun convertBack(target: String) = target.toInt() 74 | 75 | override fun convert(source: Int) = source.toString() 76 | } 77 | ) 78 | } 79 | 80 | companion object { 81 | 82 | const val FOO = "foo" 83 | const val ONE = "1" 84 | const val TWO = "2" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/binding/CollectionBindingsTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.binding.bindContainsAllWith 4 | import org.hexworks.cobalt.databinding.api.binding.bindContainsWith 5 | import org.hexworks.cobalt.databinding.api.binding.bindFlatten 6 | import org.hexworks.cobalt.databinding.api.binding.bindIndexOfWith 7 | import org.hexworks.cobalt.databinding.api.binding.bindIsEmpty 8 | import org.hexworks.cobalt.databinding.api.binding.bindIsEqualToWith 9 | import org.hexworks.cobalt.databinding.api.binding.bindLastIndexOfWith 10 | import org.hexworks.cobalt.databinding.api.binding.bindMinusWith 11 | import org.hexworks.cobalt.databinding.api.binding.bindPlusWith 12 | import org.hexworks.cobalt.databinding.api.binding.bindSize 13 | import org.hexworks.cobalt.databinding.api.extension.ObservablePersistentCollection 14 | import org.hexworks.cobalt.databinding.api.extension.toProperty 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @Suppress("FunctionName", "TestFunctionName") 19 | class CollectionBindingsTest { 20 | 21 | private val prop1To3 = NUMBERS_1_TO_3.toProperty() 22 | private val prop4To6 = NUMBERS_4_TO_6.toProperty() 23 | private val prop7To9 = NUMBERS_7_TO_9.toProperty() 24 | 25 | @Test 26 | fun When_list_properties_are_flattened_Then_the_binding_value_is_properly_calculated() { 27 | val a = mutableListOf(1, 2, 3).toProperty() 28 | val b = mutableListOf(4, 5, 6).toProperty() 29 | 30 | val fm = mutableListOf>().toProperty() 31 | 32 | val result = fm.bindFlatten() 33 | 34 | fm.add(a) 35 | fm.add(b) 36 | 37 | assertEquals( 38 | expected = a.value + b.value, 39 | actual = result.value 40 | ) 41 | } 42 | 43 | @Test 44 | fun When_list_properties_are_flattened_Then_the_binding_value_is_properly_calculated_when_items_are_removed() { 45 | val a = mutableListOf(1, 2, 3).toProperty() 46 | val b = mutableListOf(4, 5, 6).toProperty() 47 | 48 | val fm = mutableListOf>().toProperty() 49 | 50 | val result = fm.bindFlatten() 51 | 52 | fm.add(a) 53 | fm.add(b) 54 | 55 | fm.remove(a) 56 | 57 | assertEquals( 58 | expected = b.value, 59 | actual = result.value 60 | ) 61 | } 62 | 63 | @Test 64 | fun When_list_properties_are_plussed_Then_the_binding_value_is_the_plussed_list() { 65 | 66 | val binding = prop1To3 bindPlusWith prop4To6 67 | 68 | assertEquals( 69 | expected = NUMBERS_1_TO_3 + NUMBERS_4_TO_6, 70 | actual = binding.value.toList() 71 | ) 72 | } 73 | 74 | @Test 75 | fun When_list_property_and_binding_are_plussed_Then_the_binding_value_is_the_plussed_list() { 76 | 77 | val binding = prop1To3 bindPlusWith prop4To6 78 | val derivedBinding = binding bindPlusWith prop7To9 79 | 80 | assertEquals( 81 | expected = NUMBERS_1_TO_3 + NUMBERS_4_TO_6 + NUMBERS_7_TO_9, 82 | actual = derivedBinding.value.toList() 83 | ) 84 | } 85 | 86 | @Test 87 | fun When_list_properties_are_minused_Then_the_binding_value_is_the_minused_list() { 88 | 89 | val binding1To6 = prop1To3 bindPlusWith prop4To6 90 | 91 | val subtracted = binding1To6 bindMinusWith prop4To6 92 | 93 | assertEquals(NUMBERS_1_TO_3, subtracted.value) 94 | } 95 | 96 | @Test 97 | fun When_list_property_and_binding_are_minused_Then_the_binding_value_is_the_minused_list() { 98 | 99 | val binding = prop1To3 bindPlusWith prop4To6 100 | val derivedBinding = binding bindPlusWith prop7To9 101 | 102 | val subtracted = derivedBinding bindMinusWith prop7To9 103 | 104 | assertEquals(NUMBERS_1_TO_3 + NUMBERS_4_TO_6, subtracted.value.toList()) 105 | } 106 | 107 | @Test 108 | fun When_properties_are_equal_Then_derived_is_equal_binding_is_true() { 109 | 110 | val binding = prop1To3 bindIsEqualToWith prop1To3 111 | 112 | assertEquals(true, binding.value) 113 | } 114 | 115 | @Test 116 | fun When_properties_are_not_equal_Then_derived_is_equal_binding_is_false() { 117 | 118 | val binding = prop1To3 bindIsEqualToWith prop4To6 119 | 120 | assertEquals(false, binding.value) 121 | } 122 | 123 | @Test 124 | fun When_list_size_is_bound_Then_the_binding_value_is_correct() { 125 | 126 | val binding = prop1To3.bindSize() 127 | 128 | assertEquals(3, binding.value) 129 | } 130 | 131 | @Test 132 | fun When_list_size_is_bound_Then_adding_to_the_list_changes_the_binding_value() { 133 | 134 | val binding = prop1To3.bindSize() 135 | 136 | prop1To3.add(4) 137 | 138 | assertEquals(4, binding.value) 139 | } 140 | 141 | @Test 142 | fun When_list_emptiness_is_bound_Then_the_binding_value_is_correct() { 143 | 144 | val binding = prop1To3.bindIsEmpty() 145 | 146 | assertEquals(false, binding.value) 147 | } 148 | 149 | @Test 150 | fun When_list_emptiness_is_bound_and_the_list_becomes_empty_Then_the_binding_value_changes() { 151 | 152 | val binding = prop1To3.bindIsEmpty() 153 | 154 | prop1To3.clear() 155 | 156 | assertEquals(true, binding.value) 157 | } 158 | 159 | @Test 160 | fun When_list_contains_is_bound_Then_the_binding_value_is_correct() { 161 | 162 | val prop1 = 1.toProperty() 163 | val binding = prop1To3.bindContainsWith(prop1) 164 | 165 | assertEquals(true, binding.value) 166 | } 167 | 168 | @Test 169 | fun When_list_contains_is_bound_and_the_number_changes_Then_the_binding_value_changes() { 170 | 171 | val prop1 = 1.toProperty() 172 | val binding = prop1To3.bindContainsWith(prop1) 173 | 174 | prop1.value = 4 175 | 176 | assertEquals(false, binding.value) 177 | } 178 | 179 | @Test 180 | fun When_list_contains_all_is_bound_Then_the_binding_value_is_correct() { 181 | 182 | val listProp = listOf(1, 2, 3).toProperty() 183 | val binding = prop1To3.bindContainsAllWith(listProp) 184 | 185 | assertEquals(true, binding.value) 186 | } 187 | 188 | @Test 189 | fun When_list_contains_all_is_bound_and_the_list_changes_Then_the_binding_value_changes() { 190 | 191 | val listProp = listOf(1, 2, 3).toProperty() 192 | val binding = prop1To3.bindContainsAllWith(listProp) 193 | 194 | listProp.add(4) 195 | 196 | assertEquals(false, binding.value) 197 | } 198 | 199 | @Test 200 | fun When_list_index_of_is_bound_Then_the_binding_value_is_correct() { 201 | 202 | val numProp = 1.toProperty() 203 | val binding = prop1To3.bindIndexOfWith(numProp) 204 | 205 | assertEquals(0, binding.value) 206 | } 207 | 208 | @Test 209 | fun When_list_index_of_is_bound_and_the_number_changes_Then_the_binding_value_changes() { 210 | 211 | val numProp = 1.toProperty() 212 | val binding = prop1To3.bindIndexOfWith(numProp) 213 | 214 | numProp.value = 2 215 | 216 | assertEquals(1, binding.value) 217 | } 218 | 219 | @Test 220 | fun When_list_last_index_of_is_bound_Then_the_binding_value_is_correct() { 221 | 222 | val numProp = 1.toProperty() 223 | val binding = prop1To3.bindLastIndexOfWith(numProp) 224 | 225 | assertEquals(0, binding.value) 226 | } 227 | 228 | @Test 229 | fun When_list_last_index_of_is_bound_and_the_number_changes_Then_the_binding_value_changes() { 230 | 231 | val numProp = 1.toProperty() 232 | val binding = prop1To3.bindLastIndexOfWith(numProp) 233 | 234 | prop1To3.add(1) 235 | 236 | assertEquals(3, binding.value) 237 | } 238 | 239 | companion object { 240 | 241 | val NUMBERS_1_TO_3: Collection = listOf(1, 2, 3) 242 | val NUMBERS_4_TO_6: Collection = listOf(4, 5, 6) 243 | val NUMBERS_7_TO_9: Collection = listOf(7, 8, 9) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/binding/ComputedDualBindingTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.binding 2 | 3 | import org.hexworks.cobalt.databinding.api.extension.toProperty 4 | import kotlin.test.BeforeTest 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertFailsWith 8 | import kotlin.test.assertFalse 9 | import kotlin.test.assertTrue 10 | 11 | @Suppress("FunctionName", "TestFunctionName") 12 | class ComputedDualBindingTest { 13 | 14 | private lateinit var target: ComputedDualBinding 15 | 16 | private val stringValue = ONE.toProperty() 17 | private val intValue = 1.toProperty() 18 | 19 | @BeforeTest 20 | fun Set_up() { 21 | target = ComputedDualBinding(stringValue, intValue) { str, int -> 22 | str.toInt() == int 23 | } 24 | } 25 | 26 | @Test 27 | fun When_creating_a_computed_binding_its_initial_value_should_be_calculated_properly() { 28 | assertEquals(expected = true, actual = target.value) 29 | } 30 | 31 | @Test 32 | fun When_string_value_changes_to_a_value_which_is_not_equal_to_int_value_binding_value_should_change_to_false() { 33 | stringValue.value = TWO 34 | 35 | assertEquals(expected = false, actual = target.value) 36 | } 37 | 38 | @Test 39 | fun When_int_value_changes_to_a_value_which_is_not_equal_to_string_value_binding_value_should_change_to_false() { 40 | intValue.value = 2 41 | 42 | assertEquals(expected = false, actual = target.value) 43 | } 44 | 45 | @Test 46 | fun When_int_value_changes_back_to_be_equal_with_string_value_binding_should_be_updated_to_true() { 47 | intValue.value = 2 48 | intValue.value = 1 49 | 50 | assertEquals(expected = true, actual = target.value) 51 | } 52 | 53 | @Test 54 | fun When_binding_is_disposed_it_should_throw_exception_when_value_is_accessed() { 55 | target.dispose() 56 | 57 | assertFailsWith { 58 | target.value 59 | } 60 | } 61 | 62 | @Test 63 | fun When_int_value_changes_on_change_subscribers_should_be_notified() { 64 | var notified = false 65 | 66 | target.onChange { 67 | notified = true 68 | } 69 | 70 | intValue.value = 4 71 | 72 | assertTrue(notified, "No notification happened.") 73 | } 74 | 75 | @Test 76 | fun When_string_value_changes_on_change_subscribers_should_be_notified() { 77 | var notified = false 78 | 79 | target.onChange { 80 | notified = true 81 | } 82 | 83 | stringValue.value = TWO 84 | 85 | assertTrue(notified, "No notification happened.") 86 | } 87 | 88 | @Test 89 | fun When_value_does_not_change_on_change_subscribers_shouldnt_be_notified() { 90 | var notified = false 91 | 92 | target.onChange { 93 | notified = true 94 | } 95 | 96 | stringValue.value = ONE 97 | 98 | assertFalse(notified, "No notification should have happened.") 99 | } 100 | 101 | companion object { 102 | 103 | const val ONE = "1" 104 | const val TWO = "2" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultListPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.collections 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import kotlinx.collections.immutable.persistentListOf 5 | import org.hexworks.cobalt.databinding.api.binding.bindMap 6 | import org.hexworks.cobalt.databinding.api.binding.bindPlusWith 7 | import org.hexworks.cobalt.databinding.api.collection.ObservableList 8 | import org.hexworks.cobalt.databinding.api.event.ListAdd 9 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 10 | import org.hexworks.cobalt.databinding.api.extension.toProperty 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | 14 | @Suppress("TestFunctionName") 15 | open class DefaultListPropertyTest { 16 | 17 | val target = NUMBERS_1_TO_3.toProperty() 18 | 19 | @Test 20 | fun When_a_number_is_added_to_target_Then_its_value_changes() { 21 | target.add(4) 22 | 23 | assertEquals(NUMBERS_1_TO_4, target.value) 24 | } 25 | 26 | @Test 27 | fun When_target_changes_Then_its_change_is_emitted_as_an_event() { 28 | var newValue = listOf() 29 | 30 | target.onChange { 31 | newValue = it.newValue 32 | } 33 | 34 | target.add(4) 35 | 36 | assertEquals(NUMBERS_1_TO_4, newValue) 37 | } 38 | 39 | @Test 40 | fun When_a_hierarchical_list_binding_is_created_Then_the_value_is_correct() { 41 | 42 | val list0 = listOf(1).toProperty() 43 | val list1 = listOf(2).toProperty() 44 | val list2 = listOf(3).toProperty() 45 | 46 | val binding = (list0 bindPlusWith list1) bindPlusWith list2 47 | 48 | assertEquals(listOf(1, 2, 3), binding.value) 49 | 50 | } 51 | 52 | @Test 53 | fun When_the_lists_in_a_hierarchical_list_binding_are_modified_Then_the_value_stays_correct() { 54 | 55 | val list0 = listOf(1).toProperty() 56 | val list1 = listOf(3).toProperty() 57 | val list2 = listOf(5).toProperty() 58 | 59 | val binding = (list0 bindPlusWith list1) bindPlusWith list2 60 | 61 | list0.add(2) 62 | list1.add(4) 63 | list2.add(6) 64 | 65 | assertEquals(listOf(1, 2, 3, 4, 5, 6), binding.value) 66 | } 67 | 68 | @Test 69 | fun When_target_is_bound_to_other_Then_its_value_changes() { 70 | val other = NUMBERS_1_TO_4.toProperty() 71 | 72 | target.bind(other) 73 | 74 | assertEquals(NUMBERS_1_TO_4, target.value) 75 | } 76 | 77 | @Test 78 | fun When_a_list_transform_binding_is_created_Then_its_value_is_correct() { 79 | val prop = listOf(1, 2).toProperty() 80 | 81 | val binding = prop.bindMap { 82 | it.toString() 83 | } 84 | 85 | assertEquals(listOf("1", "2"), binding.value) 86 | } 87 | 88 | @Test 89 | fun Given_a_list_transform_binding_When_the_underlying_list_changes_Then_its_value_is_correct() { 90 | val prop = listOf(1, 2).toProperty() 91 | 92 | val binding = prop.bindMap { 93 | it.toString() 94 | } 95 | 96 | prop.add(3) 97 | 98 | assertEquals(listOf("1", "2", "3"), binding.value) 99 | } 100 | 101 | @Test 102 | fun Given_a_list_transform_binding_When_the_underlying_list_changes_Then_the_emitted_event_is_correct() { 103 | val prop = listOf(1, 2).toProperty() 104 | 105 | val binding = prop.bindMap { 106 | it.toString() 107 | } 108 | 109 | val changes = mutableListOf>>() 110 | 111 | binding.onChange { 112 | changes.add(it) 113 | } 114 | 115 | prop.add(3) 116 | 117 | assertEquals(1, changes.size) 118 | 119 | val change = changes.first() 120 | 121 | assertEquals( 122 | expected = ObservableValueChanged( 123 | oldValue = persistentListOf("1", "2"), 124 | newValue = persistentListOf("1", "2", "3"), 125 | observableValue = change.observableValue, 126 | type = ListAdd(3), 127 | emitter = change.emitter, 128 | trace = change.trace 129 | ), 130 | actual = change 131 | ) 132 | } 133 | 134 | @Test 135 | fun Given_a_hierarchical_list_transform_binding_When_the_underlying_list_changes_Then_its_value_is_correct() { 136 | val prop = listOf(1, 2).toProperty() 137 | 138 | val binding = prop.bindMap { 139 | it.toString() 140 | } 141 | 142 | val derived = binding bindPlusWith listOf("foo").toProperty() 143 | 144 | prop.add(3) 145 | 146 | assertEquals(listOf("1", "2", "3", "foo"), derived.value) 147 | } 148 | 149 | @Test 150 | fun Given_a_array_of_lists_to_bind_When_they_are_bound_Then_the_final_binding_is_correct() { 151 | 152 | val prop0: ObservableList = listOf(0).toProperty() 153 | val prop1: ObservableList = listOf(1).toProperty() 154 | val prop2: ObservableList = listOf(2).toProperty() 155 | val prop3: ObservableList = listOf(3).toProperty() 156 | val prop4: ObservableList = listOf(4).toProperty() 157 | 158 | val propsList = listOf(prop0, prop1, prop2, prop3, prop4) 159 | 160 | val binding = propsList.reduce { acc, observableList -> acc bindPlusWith observableList } 161 | 162 | assertEquals(persistentListOf(0, 1, 2, 3, 4), binding.value) 163 | } 164 | 165 | @Test 166 | fun Given_a_array_of_lists_to_bind_When_they_are_modified_Then_the_final_binding_is_correct() { 167 | 168 | val prop0: ObservableList = listOf(0).toProperty() 169 | val prop1: ObservableList = listOf(1).toProperty() 170 | val prop2: ObservableList = listOf(2).toProperty() 171 | val prop3: ObservableList = listOf(3).toProperty() 172 | val prop4: ObservableList = listOf(5).toProperty() 173 | 174 | val propsList = listOf(prop0, prop1, prop2, prop3, prop4) 175 | 176 | val binding = propsList.reduce { acc, observableList -> acc bindPlusWith observableList } 177 | 178 | prop3.add(4) 179 | 180 | assertEquals(persistentListOf(0, 1, 2, 3, 4, 5), binding.value) 181 | } 182 | 183 | @Test 184 | fun Given_a_map_transformation_When_an_element_is_set_Then_only_one_transformation_is_executed() { 185 | val prop: ObservableList = listOf(0, 1, 2).toProperty() 186 | 187 | var transformationCount = 0 188 | 189 | prop.bindMap { 190 | transformationCount++ 191 | it + 1 192 | } 193 | 194 | transformationCount = 0 195 | 196 | prop.set(0, 5) 197 | 198 | assertEquals(1, transformationCount) 199 | } 200 | 201 | companion object { 202 | val NUMBERS_1_TO_3 = listOf(1, 2, 3) 203 | val NUMBERS_1_TO_4 = listOf(1, 2, 3, 4) 204 | } 205 | } 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultMapPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.collections 2 | 3 | import org.hexworks.cobalt.databinding.api.collection.MapProperty 4 | import org.hexworks.cobalt.databinding.api.extension.toProperty 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | @Suppress("TestFunctionName") 9 | class DefaultMapPropertyTest { 10 | 11 | private val target: MapProperty = NUMBERS_1_TO_3.toProperty() 12 | 13 | @Test 14 | fun When_a_number_is_added_to_target_Then_its_value_changes() { 15 | target.put(4, 4) 16 | 17 | assertEquals(NUMBERS_1_TO_4, target.value) 18 | } 19 | 20 | @Test 21 | fun When_target_changes_Then_its_change_is_emitted_as_an_event() { 22 | var newValue = mapOf() 23 | 24 | target.onChange { 25 | newValue = it.newValue 26 | } 27 | 28 | target.put(4, 4) 29 | 30 | assertEquals(NUMBERS_1_TO_4, newValue) 31 | } 32 | 33 | @Test 34 | fun When_target_is_bound_to_other_Then_its_value_changes() { 35 | val other = NUMBERS_1_TO_4.toProperty() 36 | 37 | target.bind(other) 38 | 39 | assertEquals(NUMBERS_1_TO_4, target.value) 40 | } 41 | 42 | companion object { 43 | val NUMBERS_1_TO_3 = mapOf(1 to 1, 2 to 2, 3 to 3) 44 | val NUMBERS_1_TO_4 = mapOf(1 to 1, 2 to 2, 3 to 3, 4 to 4) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultPropertyListPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.collections 2 | 3 | import kotlinx.collections.immutable.PersistentList 4 | import kotlinx.collections.immutable.persistentListOf 5 | import org.hexworks.cobalt.databinding.api.binding.bindMap 6 | import org.hexworks.cobalt.databinding.api.binding.bindPlusWith 7 | import org.hexworks.cobalt.databinding.api.collection.ListProperty 8 | import org.hexworks.cobalt.databinding.api.event.ListPropertyChange 9 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 10 | import org.hexworks.cobalt.databinding.api.extension.toProperty 11 | import org.hexworks.cobalt.databinding.api.property.Property 12 | import kotlin.test.Test 13 | import kotlin.test.assertEquals 14 | import kotlin.test.assertTrue 15 | 16 | @Suppress("TestFunctionName") 17 | class DefaultPropertyListPropertyTest { 18 | 19 | val target: ListProperty> = NUMBERS_1_TO_3.toProperty() 20 | 21 | @Test 22 | fun When_a_number_is_added_to_target_Then_its_value_changes() { 23 | target.add(4.toProperty()) 24 | 25 | assertEquals(NUMBERS_1_TO_4, target.value.map { it.value }) 26 | } 27 | 28 | @Test 29 | fun When_target_changes_Then_its_change_is_emitted_as_an_event() { 30 | var newValue = listOf>() 31 | 32 | target.onChange { 33 | newValue = it.newValue 34 | } 35 | 36 | target.add(4.toProperty()) 37 | 38 | assertEquals(NUMBERS_1_TO_4.map { it }, newValue.map { it.value }) 39 | } 40 | 41 | @Test 42 | fun When_a_hierarchical_list_binding_is_created_Then_the_value_is_correct() { 43 | 44 | val list0 = listOf(1.toProperty()).toProperty() 45 | val list1 = listOf(2.toProperty()).toProperty() 46 | val list2 = listOf(3.toProperty()).toProperty() 47 | 48 | val binding = (list0 bindPlusWith list1) bindPlusWith list2 49 | 50 | assertEquals(persistentListOf(1, 2, 3), binding.value.map { it.value }) 51 | } 52 | 53 | @Test 54 | fun When_the_lists_in_a_hierarchical_list_binding_are_modified_Then_the_value_stays_correct() { 55 | 56 | val list0 = listOf(1).toProperty() 57 | val list1 = listOf(3).toProperty() 58 | val list2 = listOf(5).toProperty() 59 | 60 | val binding = (list0 bindPlusWith list1) bindPlusWith list2 61 | 62 | list0.add(2) 63 | list1.add(4) 64 | list2.add(6) 65 | 66 | assertEquals(listOf(1, 2, 3, 4, 5, 6), binding.value) 67 | } 68 | 69 | @Test 70 | fun When_target_is_bound_to_other_Then_its_value_changes() { 71 | val other = NUMBERS_1_TO_4.map { it.toProperty() }.toProperty() 72 | 73 | target.bind(other) 74 | 75 | assertEquals(other.value, target.value) 76 | } 77 | 78 | @Test 79 | fun When_a_list_transform_binding_is_created_Then_its_value_is_correct() { 80 | val prop = listOf(1.toProperty(), 2.toProperty()).toProperty() 81 | 82 | val binding = prop.bindMap { 83 | it.value.toString() 84 | } 85 | 86 | assertEquals(listOf("1", "2"), binding.value) 87 | } 88 | 89 | @Test 90 | fun Given_a_list_transform_binding_When_the_underlying_list_changes_Then_its_value_is_correct() { 91 | val prop = listOf(1.toProperty(), 2.toProperty()).toProperty() 92 | 93 | val binding = prop.bindMap { 94 | it.value.toString() 95 | } 96 | 97 | prop.add(3.toProperty()) 98 | 99 | assertEquals(listOf("1", "2", "3"), binding.value) 100 | } 101 | 102 | @Test 103 | fun Given_a_list_transform_binding_When_the_underlying_list_changes_Then_the_emitted_event_is_correct() { 104 | val prop = listOf(1.toProperty(), 2.toProperty()).toProperty() 105 | 106 | val binding = prop.bindMap { 107 | it.value.toString() 108 | } 109 | 110 | val listChanges = mutableListOf>>>() 111 | val changes = mutableListOf>>() 112 | 113 | prop.onChange { 114 | listChanges.add(it) 115 | } 116 | 117 | binding.onChange { 118 | changes.add(it) 119 | } 120 | 121 | prop.add(3.toProperty()) 122 | 123 | assertEquals(1, changes.size) 124 | 125 | val change = changes.first() 126 | 127 | assertEquals( 128 | expected = ObservableValueChanged( 129 | oldValue = persistentListOf("1", "2"), 130 | newValue = persistentListOf("1", "2", "3"), 131 | observableValue = change.observableValue, 132 | type = listChanges.first().type, 133 | emitter = change.emitter, 134 | trace = change.trace 135 | ), 136 | actual = change 137 | ) 138 | } 139 | 140 | @Test 141 | fun Given_a_hierarchical_list_transform_binding_When_the_underlying_list_changes_Then_its_value_is_correct() { 142 | val prop = listOf(1.toProperty(), 2.toProperty()).toProperty() 143 | 144 | val binding = prop.bindMap { 145 | it.value.toString() 146 | } 147 | 148 | val derived = binding bindPlusWith listOf("foo").toProperty() 149 | 150 | prop.add(3.toProperty()) 151 | 152 | assertEquals(listOf("1", "2", "3", "foo"), derived.value) 153 | } 154 | 155 | @Test 156 | fun Given_a_map_transformation_When_an_element_is_set_Then_only_one_transformation_is_executed() { 157 | val prop = listOf(0.toProperty(), 1.toProperty(), 2.toProperty()).toProperty() 158 | 159 | var transformationCount = 0 160 | 161 | prop.bindMap { 162 | transformationCount++ 163 | it.value + 1 164 | } 165 | 166 | transformationCount = 0 167 | 168 | prop.set(0, 5.toProperty()) 169 | 170 | assertEquals(1, transformationCount) 171 | } 172 | 173 | @Test 174 | fun Given_a_property_list_When_a_property_changes_Then_the_proper_event_is_fired() { 175 | val prop = target[0] 176 | var change: ObservableValueChanged>>? = null 177 | var innerChange: ObservableValueChanged? = null 178 | prop.onChange { 179 | innerChange = it 180 | } 181 | target.onChange { 182 | change = it 183 | } 184 | prop.value = 0 185 | 186 | assertTrue { 187 | change?.type is ListPropertyChange<*> 188 | } 189 | assertEquals(innerChange!!, (change!!.type as ListPropertyChange<*>).changeEvent) 190 | } 191 | 192 | @Test 193 | fun Given_a_property_list_When_a_property_is_added_and_changes_Then_the_proper_event_is_fired() { 194 | val prop = 4.toProperty() 195 | var change: ObservableValueChanged>>? = null 196 | var innerChange: ObservableValueChanged? = null 197 | prop.onChange { 198 | innerChange = it 199 | } 200 | target.onChange { 201 | change = it 202 | } 203 | target.add(prop) 204 | prop.value = 0 205 | 206 | assertTrue { 207 | change?.type is ListPropertyChange<*> 208 | } 209 | assertEquals(innerChange!!, (change!!.type as ListPropertyChange<*>).changeEvent) 210 | } 211 | 212 | @Test 213 | fun Given_a_property_list_When_a_property_is_added_and_removed_and_changes_Then_there_is_no_event_fired() { 214 | val prop = 4.toProperty() 215 | var change: ObservableValueChanged>>? = null 216 | target.add(prop) 217 | target.remove(prop) 218 | target.onChange { 219 | change = it 220 | } 221 | prop.value = 0 222 | 223 | assertEquals(null, change) 224 | } 225 | 226 | companion object { 227 | val NUMBERS_1_TO_3 = listOf(1.toProperty(), 2.toProperty(), 3.toProperty()) 228 | val NUMBERS_1_TO_4 = listOf(1, 2, 3, 4) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultPropertySetPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.collections 2 | 3 | import kotlinx.collections.immutable.PersistentSet 4 | import kotlinx.collections.immutable.persistentListOf 5 | import kotlinx.collections.immutable.persistentSetOf 6 | import org.hexworks.cobalt.databinding.api.binding.bindMap 7 | import org.hexworks.cobalt.databinding.api.binding.bindPlusWith 8 | import org.hexworks.cobalt.databinding.api.collection.SetProperty 9 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 10 | import org.hexworks.cobalt.databinding.api.event.SetPropertyChange 11 | import org.hexworks.cobalt.databinding.api.extension.toProperty 12 | import org.hexworks.cobalt.databinding.api.property.Property 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.test.assertTrue 16 | 17 | @Suppress("TestFunctionName") 18 | class DefaultPropertySetPropertyTest { 19 | 20 | val target: SetProperty> = NUMBERS_1_TO_3.toProperty() 21 | 22 | @Test 23 | fun When_a_number_is_added_to_target_Then_its_value_changes() { 24 | target.add(4.toProperty()) 25 | 26 | assertEquals(NUMBERS_1_TO_4.map { it }, target.value.map { it.value }) 27 | } 28 | 29 | @Test 30 | fun When_target_changes_Then_its_change_is_emitted_as_an_event() { 31 | var newValue = setOf>() 32 | 33 | target.onChange { 34 | newValue = it.newValue 35 | } 36 | 37 | target.add(4.toProperty()) 38 | 39 | assertEquals(NUMBERS_1_TO_4.map { it }, newValue.map { it.value }) 40 | } 41 | 42 | @Test 43 | fun When_a_hierarchical_list_binding_is_created_Then_the_value_is_correct() { 44 | 45 | val list0 = listOf(1.toProperty()).toProperty() 46 | val list1 = listOf(2.toProperty()).toProperty() 47 | val list2 = listOf(3.toProperty()).toProperty() 48 | 49 | val binding = (list0 bindPlusWith list1) bindPlusWith list2 50 | 51 | assertEquals(persistentListOf(1, 2, 3), binding.value.map { it.value }) 52 | } 53 | 54 | @Test 55 | fun When_the_lists_in_a_hierarchical_list_binding_are_modified_Then_the_value_stays_correct() { 56 | 57 | val set0 = setOf(1).toProperty() 58 | val set1 = setOf(3).toProperty() 59 | val set2 = setOf(5).toProperty() 60 | 61 | val binding = (set0 bindPlusWith set1) bindPlusWith set2 62 | 63 | set0.add(2) 64 | set1.add(4) 65 | set2.add(6) 66 | 67 | assertEquals(setOf(1, 2, 3, 4, 5, 6), binding.value) 68 | } 69 | 70 | @Test 71 | fun When_target_is_bound_to_other_Then_its_value_changes() { 72 | val other = NUMBERS_1_TO_4.map { it.toProperty() }.toSet().toProperty() 73 | 74 | target.bind(other) 75 | 76 | assertEquals(other.value, target.value) 77 | } 78 | 79 | @Test 80 | fun When_a_list_transform_binding_is_created_Then_its_value_is_correct() { 81 | val prop = setOf(1.toProperty(), 2.toProperty()).toProperty() 82 | 83 | val binding = prop.bindMap { 84 | it.value.toString() 85 | } 86 | 87 | assertEquals(setOf("1", "2"), binding.value) 88 | } 89 | 90 | @Test 91 | fun Given_a_list_transform_binding_When_the_underlying_list_changes_Then_its_value_is_correct() { 92 | val prop = setOf(1.toProperty(), 2.toProperty()).toProperty() 93 | 94 | val binding = prop.bindMap { 95 | it.value.toString() 96 | } 97 | 98 | prop.add(3.toProperty()) 99 | 100 | assertEquals(setOf("1", "2", "3"), binding.value) 101 | } 102 | 103 | @Test 104 | fun Given_a_list_transform_binding_When_the_underlying_list_changes_Then_the_emitted_event_is_correct() { 105 | val prop = setOf(1.toProperty(), 2.toProperty()).toProperty() 106 | 107 | val binding = prop.bindMap { 108 | it.value.toString() 109 | } 110 | 111 | val changes = mutableListOf>>() 112 | val setChanges = mutableListOf>>>() 113 | 114 | prop.onChange { 115 | setChanges.add(it) 116 | } 117 | 118 | binding.onChange { 119 | changes.add(it) 120 | } 121 | 122 | prop.add(3.toProperty()) 123 | 124 | assertEquals(1, changes.size) 125 | 126 | val change = changes.first() 127 | 128 | assertEquals( 129 | expected = ObservableValueChanged( 130 | oldValue = persistentSetOf("1", "2"), 131 | newValue = persistentSetOf("1", "2", "3"), 132 | observableValue = change.observableValue, 133 | type = setChanges.first().type, 134 | emitter = change.emitter, 135 | trace = change.trace 136 | ), 137 | actual = change 138 | ) 139 | } 140 | 141 | @Test 142 | fun Given_a_hierarchical_list_transform_binding_When_the_underlying_list_changes_Then_its_value_is_correct() { 143 | val prop = setOf(1.toProperty(), 2.toProperty()).toProperty() 144 | 145 | val binding = prop.bindMap { 146 | it.value.toString() 147 | } 148 | 149 | val derived = binding bindPlusWith setOf("foo").toProperty() 150 | 151 | prop.add(3.toProperty()) 152 | 153 | assertEquals(setOf("1", "2", "3", "foo"), derived.value) 154 | } 155 | 156 | @Test 157 | fun Given_a_property_list_When_a_property_changes_Then_the_proper_event_is_fired() { 158 | val prop = target.first() 159 | var change: ObservableValueChanged>>? = null 160 | var innerChange: ObservableValueChanged? = null 161 | prop.onChange { 162 | innerChange = it 163 | } 164 | target.onChange { 165 | change = it 166 | } 167 | prop.value = 0 168 | 169 | assertTrue { 170 | println(change) 171 | change?.type is SetPropertyChange<*> 172 | } 173 | assertEquals(innerChange!!, (change!!.type as SetPropertyChange<*>).changeEvent) 174 | } 175 | 176 | @Test 177 | fun Given_a_property_list_When_a_property_is_added_and_changes_Then_the_proper_event_is_fired() { 178 | val prop = 4.toProperty() 179 | var change: ObservableValueChanged>>? = null 180 | var innerChange: ObservableValueChanged? = null 181 | prop.onChange { 182 | innerChange = it 183 | } 184 | target.onChange { 185 | change = it 186 | } 187 | target.add(prop) 188 | prop.value = 0 189 | 190 | assertTrue { 191 | change?.type is SetPropertyChange<*> 192 | } 193 | assertEquals(innerChange!!, (change!!.type as SetPropertyChange<*>).changeEvent) 194 | } 195 | 196 | @Test 197 | fun Given_a_property_list_When_a_property_is_added_and_removed_and_changes_Then_there_is_no_event_fired() { 198 | val prop = 4.toProperty() 199 | var change: ObservableValueChanged>>? = null 200 | target.add(prop) 201 | target.remove(prop) 202 | target.onChange { 203 | change = it 204 | } 205 | prop.value = 0 206 | 207 | assertEquals(null, change) 208 | } 209 | 210 | companion object { 211 | val NUMBERS_1_TO_3 = setOf(1.toProperty(), 2.toProperty(), 3.toProperty()) 212 | val NUMBERS_1_TO_4 = setOf(1, 2, 3, 4) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/collections/DefaultSetPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.collections 2 | 3 | import org.hexworks.cobalt.databinding.api.extension.toProperty 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | @Suppress("TestFunctionName") 8 | class DefaultSetPropertyTest { 9 | 10 | private val target = NUMBERS_1_TO_3.toProperty() 11 | 12 | @Test 13 | fun When_a_number_is_added_to_target_Then_its_value_changes() { 14 | target.add(4) 15 | 16 | assertEquals(NUMBERS_1_TO_4, target.value) 17 | } 18 | 19 | @Test 20 | fun When_target_changes_Then_its_change_is_emitted_as_an_event() { 21 | var newValue = setOf() 22 | 23 | target.onChange { 24 | newValue = it.newValue 25 | } 26 | 27 | target.add(4) 28 | 29 | assertEquals(NUMBERS_1_TO_4, newValue) 30 | } 31 | 32 | @Test 33 | fun When_target_is_bound_to_other_Then_its_value_changes() { 34 | val other = NUMBERS_1_TO_4.toProperty() 35 | 36 | target.bind(other) 37 | 38 | assertEquals(NUMBERS_1_TO_4, target.value) 39 | } 40 | 41 | companion object { 42 | val NUMBERS_1_TO_3 = setOf(1, 2, 3) 43 | val NUMBERS_1_TO_4 = setOf(1, 2, 3, 4) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/expression/BooleanExpressionsTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.expression 2 | 3 | @Suppress("FunctionName") 4 | class BooleanExpressionsTest 5 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/expression/LongExpressionsTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.expression 2 | 3 | import org.hexworks.cobalt.databinding.api.binding.bindMinusWith 4 | import org.hexworks.cobalt.databinding.api.binding.bindNegate 5 | import org.hexworks.cobalt.databinding.api.binding.bindPlusWith 6 | import org.hexworks.cobalt.databinding.api.binding.bindTimesWith 7 | import org.hexworks.cobalt.databinding.api.extension.toProperty 8 | import org.hexworks.cobalt.databinding.api.property.Property 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | @Suppress("TestFunctionName") 13 | class LongExpressionsTest { 14 | 15 | @Test 16 | fun When_property_is_negated_binding_value_should_be_negative() { 17 | val prop: Property = 2L.toProperty() 18 | 19 | val binding = prop.bindNegate() 20 | 21 | assertEquals(expected = -2, actual = binding.value) 22 | } 23 | 24 | @Test 25 | fun When_property_is_added_to_other_property_binding_value_should_be_their_sum() { 26 | val prop: Property = 2L.toProperty() 27 | val otherProp: Property = 4L.toProperty() 28 | 29 | val sum = prop.bindPlusWith(otherProp) 30 | 31 | assertEquals(expected = 6, actual = sum.value) 32 | } 33 | 34 | @Test 35 | fun When_sum_binding_is_created_and_property_changes_sum_should_also_change() { 36 | val prop: Property = 2L.toProperty() 37 | val otherProp: Property = 4L.toProperty() 38 | 39 | val sum = prop.bindPlusWith(otherProp) 40 | 41 | prop.value = 5 42 | 43 | assertEquals(expected = 9, actual = sum.value) 44 | } 45 | 46 | @Test 47 | fun When_complex_binding_is_created_and_property_changes_binding_should_also_change() { 48 | val sumPart0: Property = 2L.toProperty() 49 | val sumPart1: Property = 4L.toProperty() 50 | 51 | val diffPart0: Property = 8L.toProperty() 52 | val diffPart1: Property = 2L.toProperty() 53 | 54 | val sum = sumPart0.bindPlusWith(sumPart1) 55 | val diff = diffPart0.bindMinusWith(diffPart1) 56 | 57 | val complexBinding = sum.bindTimesWith(diff) 58 | 59 | assertEquals(expected = 36, actual = complexBinding.value) 60 | 61 | diffPart1.value = 7 62 | 63 | assertEquals(expected = 6, actual = complexBinding.value) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/expression/StringExpressionsTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("TestFunctionName") 2 | 3 | package org.hexworks.cobalt.databinding.internal.expression 4 | 5 | import org.hexworks.cobalt.databinding.api.binding.bindIsEmpty 6 | import org.hexworks.cobalt.databinding.api.binding.bindNot 7 | import org.hexworks.cobalt.databinding.api.extension.toProperty 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | 11 | @Suppress("FunctionName") 12 | class StringExpressionsTest { 13 | 14 | @Test 15 | fun When_property_is_empty_binding_should_have_true_value() { 16 | val prop = "".toProperty() 17 | 18 | val binding = prop.bindIsEmpty() 19 | 20 | assertEquals(expected = true, actual = binding.value) 21 | } 22 | 23 | @Test 24 | fun When_property_is_no_longer_empty_binding_should_have_false_value() { 25 | val prop = "".toProperty() 26 | 27 | val binding = prop.bindIsEmpty() 28 | 29 | prop.value = "foo" 30 | 31 | assertEquals(expected = false, actual = binding.value) 32 | } 33 | 34 | @Test 35 | fun When_property_is_not_empty_binding_should_have_true_value() { 36 | val prop = "".toProperty() 37 | 38 | val binding = prop.bindIsEmpty().bindNot() 39 | 40 | assertEquals(expected = false, actual = binding.value) 41 | } 42 | 43 | @Test 44 | fun When_property_is_empty_binding_should_have_false_value() { 45 | val prop = "".toProperty() 46 | 47 | val binding = prop.bindIsEmpty().bindNot() 48 | 49 | prop.value = "foo" 50 | 51 | assertEquals(expected = true, actual = binding.value) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/property/DefaultPropertyDelegateTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.property 2 | 3 | import org.hexworks.cobalt.databinding.api.extension.toProperty 4 | import org.hexworks.cobalt.databinding.api.property.Property 5 | import kotlin.test.BeforeTest 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | @Suppress("FunctionName", "TestFunctionName") 10 | class DefaultPropertyDelegateTest { 11 | 12 | lateinit var property: Property 13 | 14 | @BeforeTest 15 | fun Set_up() { 16 | property = XUL.toProperty() 17 | } 18 | 19 | @Test 20 | fun When_property_is_delegated_to_a_variable_the_variable_should_have_the_same_value_as_the_property() { 21 | val target: String by property.asDelegate() 22 | 23 | assertEquals(expected = XUL, actual = target) 24 | } 25 | 26 | @Test 27 | fun When_a_property_is_updated_its_delegate_should_be_updated_as_well() { 28 | val target: String by property.asDelegate() 29 | 30 | property.value = QUX 31 | 32 | assertEquals(expected = QUX, actual = target) 33 | } 34 | 35 | companion object { 36 | const val XUL = "XUL" 37 | const val QUX = "QUX" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/databinding/internal/property/DefaultPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.databinding.internal.property 2 | 3 | import org.hexworks.cobalt.databinding.api.event.ObservableValueChanged 4 | import org.hexworks.cobalt.databinding.api.event.ScalarChange 5 | import org.hexworks.cobalt.databinding.api.extension.toProperty 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertTrue 9 | 10 | @Suppress("FunctionName", "TestFunctionName") 11 | class DefaultPropertyTest { 12 | 13 | private val target = XUL.toProperty() 14 | 15 | @Test 16 | fun When_target_property_value_changes_the_change_listener_should_be_notified_with_the_proper_event() { 17 | var change: ObservableValueChanged? = null 18 | val expectedChange = ObservableValueChanged( 19 | observableValue = target, 20 | oldValue = XUL, 21 | newValue = QUX, 22 | emitter = target, 23 | type = ScalarChange 24 | ) 25 | 26 | target.onChange { 27 | change = it 28 | } 29 | target.value = QUX 30 | 31 | assertTrue(change != null, "No change happened.") 32 | assertEquals( 33 | expected = expectedChange, 34 | actual = change!!, 35 | message = "Actual ChangeEvent is different from expected." 36 | ) 37 | } 38 | 39 | @Test 40 | fun When_target_property_is_bound_to_another_one_its_value_should_be_set_to_the_value_of_the_other_property() { 41 | val otherProperty = QUX.toProperty() 42 | 43 | target.bind(otherProperty) 44 | 45 | assertEquals(expected = QUX, actual = target.value, message = "Property value was not set to other value.") 46 | } 47 | 48 | @Test 49 | fun When_the_value_of_other_property_changes_a_bound_property_value_should_be_updated() { 50 | val otherProperty = QUX.toProperty() 51 | 52 | target.bind(otherProperty) 53 | 54 | otherProperty.value = BAZ 55 | 56 | assertEquals(expected = BAZ, actual = target.value, message = "Property value was not set to other value.") 57 | } 58 | 59 | @Test 60 | fun When_the_value_of_other_property_changes_all_bound_property_values_should_be_updated() { 61 | val otherProperty = QUX.toProperty() 62 | val boundProperty = QUX.toProperty() 63 | 64 | target.bind(otherProperty) 65 | boundProperty.bind(otherProperty) 66 | 67 | otherProperty.value = BAZ 68 | 69 | assertEquals(expected = BAZ, actual = target.value, message = "Property value was not set to other value.") 70 | assertEquals( 71 | expected = BAZ, 72 | actual = boundProperty.value, 73 | message = "Bound property value was not set to other value." 74 | ) 75 | } 76 | 77 | @Test 78 | fun When_creating_a_circular_binding_it_should_not_lead_to_stack_overflow() { 79 | val otherProperty0 = QUX.toProperty() 80 | val otherProperty1 = BAZ.toProperty() 81 | 82 | target.bind(otherProperty0) 83 | otherProperty0.bind(otherProperty1) 84 | otherProperty1.bind(target) 85 | 86 | assertEquals(expected = BAZ, actual = target.value) 87 | assertEquals(expected = BAZ, actual = otherProperty0.value) 88 | assertEquals(expected = BAZ, actual = otherProperty1.value) 89 | } 90 | 91 | @Test 92 | fun When_setting_a_value_in_a_circular_binding_it_should_not_lead_to_a_deadlock() { 93 | val otherProperty0 = QUX.toProperty() 94 | val otherProperty1 = BAZ.toProperty() 95 | 96 | target.bind(otherProperty0) 97 | otherProperty0.bind(otherProperty1) 98 | otherProperty1.bind(target) 99 | target.value = XUL 100 | 101 | assertEquals(expected = XUL, actual = target.value) 102 | assertEquals(expected = XUL, actual = otherProperty0.value) 103 | assertEquals(expected = XUL, actual = otherProperty1.value) 104 | } 105 | 106 | @Test 107 | fun When_binding_bidirectionally_to_another_property_target_value_should_be_updated() { 108 | val other = QUX.toProperty() 109 | 110 | target.bind(other) 111 | 112 | assertEquals(expected = QUX, actual = target.value) 113 | } 114 | 115 | @Test 116 | fun When_binding_bidirectionally_and_target_value_changes_other_should_be_updated() { 117 | val otherProperty = QUX.toProperty() 118 | 119 | target.bind(otherProperty) 120 | 121 | target.value = BAZ 122 | 123 | assertEquals(expected = BAZ, actual = otherProperty.value) 124 | } 125 | 126 | @Test 127 | fun When_binding_bidirectionally_and_other_value_changes_target_should_be_updated() { 128 | val otherProperty = QUX.toProperty() 129 | 130 | target.bind(otherProperty) 131 | 132 | otherProperty.value = BAZ 133 | 134 | assertEquals(expected = BAZ, actual = target.value) 135 | } 136 | 137 | @Test 138 | fun When_binding_bidirectionally_binding_should_have_same_value_as_target() { 139 | val otherProperty = QUX.toProperty() 140 | 141 | val binding = target.bind(otherProperty) 142 | 143 | otherProperty.value = BAZ 144 | 145 | assertEquals(expected = BAZ, actual = binding.value) 146 | } 147 | 148 | @Test 149 | fun When_binding_is_disposed_target_should_not_update_when_other_changes() { 150 | val otherProperty = QUX.toProperty() 151 | 152 | target.bind(otherProperty).dispose() 153 | 154 | otherProperty.value = BAZ 155 | 156 | assertEquals(expected = QUX, actual = target.value) 157 | } 158 | 159 | @Test 160 | fun When_binding_is_disposed_other_should_not_update_when_target_changes() { 161 | val otherProperty = QUX.toProperty() 162 | 163 | target.bind(otherProperty).dispose() 164 | 165 | target.value = BAZ 166 | 167 | assertEquals(expected = QUX, actual = otherProperty.value) 168 | } 169 | 170 | @Test 171 | fun When_bound_with_converter_target_value_should_be_updated() { 172 | val otherProperty = 1.toProperty() 173 | 174 | target.updateFrom(otherProperty) { 175 | otherProperty.value.toString() 176 | } 177 | 178 | assertEquals( 179 | expected = ONE, actual = target.value, 180 | message = "Target value should have been updated." 181 | ) 182 | } 183 | 184 | @Test 185 | fun When_bound_with_converter_and_other_changes_target_should_be_updated() { 186 | val otherProperty = 1.toProperty() 187 | 188 | target.updateFrom(otherProperty) { 189 | it.toString() 190 | } 191 | 192 | otherProperty.value = 2 193 | 194 | assertEquals( 195 | expected = TWO, actual = target.value, 196 | message = "Target value should have been updated." 197 | ) 198 | } 199 | 200 | companion object { 201 | const val XUL = "XUL" 202 | const val QUX = "QUX" 203 | const val BAZ = "BAZ" 204 | 205 | const val ONE = "1" 206 | const val TWO = "2" 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /cobalt.core/src/commonTest/kotlin/org/hexworks/cobalt/events/EventBusTest.kt: -------------------------------------------------------------------------------- 1 | package org.hexworks.cobalt.events 2 | 3 | import org.hexworks.cobalt.core.api.behavior.DisposedByException 4 | import org.hexworks.cobalt.events.api.* 5 | import org.hexworks.cobalt.events.internal.ApplicationScope 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertFalse 9 | import kotlin.test.assertTrue 10 | 11 | @Suppress("TestFunctionName") 12 | class EventBusTest { 13 | 14 | private val target = EventBus.create() 15 | 16 | @Test 17 | fun When_a_subscription_for_a_scope_and_key_is_cancelled_other_subscriptions_for_the_same_combination_shouldnt_be_cancelled() { 18 | 19 | val subscription0 = target.simpleSubscribeTo { 20 | } 21 | val subscription1 = target.simpleSubscribeTo { 22 | } 23 | 24 | subscription0.dispose() 25 | 26 | assertFalse(subscription1.disposed) 27 | } 28 | 29 | @Test 30 | fun When_a_subscription_for_a_scope_and_key_is_cancelled_other_subscriptions_for_the_same_combination_should_be_notified_when_an_event_is_published() { 31 | 32 | var sub0Notified = false 33 | var sub1Notified = false 34 | 35 | 36 | val subscription0 = target.simpleSubscribeTo { 37 | sub0Notified = true 38 | } 39 | target.simpleSubscribeTo { 40 | sub1Notified = true 41 | } 42 | 43 | subscription0.dispose() 44 | target.publish(TestEvent(this)) 45 | 46 | assertFalse(sub0Notified) 47 | assertTrue(sub1Notified) 48 | 49 | } 50 | 51 | @Test 52 | fun When_subscribed_to_an_event_and_the_proper_event_is_published_then_the_subscriber_should_be_notified() { 53 | 54 | var notified = false 55 | 56 | target.simpleSubscribeTo { 57 | notified = true 58 | } 59 | 60 | target.publish(TestEvent(this)) 61 | 62 | assertEquals(true, notified, "Subscriber was not notified.") 63 | } 64 | 65 | @Test 66 | fun When_subscribed_to_an_event_and_scope_and_the_proper_event_is_published_then_the_subscriber_should_be_notified() { 67 | 68 | var notified = false 69 | target.simpleSubscribeTo(TestScope) { 70 | notified = true 71 | } 72 | 73 | target.publish(TestEvent(this), TestScope) 74 | 75 | assertEquals(true, notified, "Subscriber was not notified.") 76 | } 77 | 78 | @Test 79 | fun When_subscribed_to_an_event_and_scope_and_the_proper_event_is_published_but_with_wrong_scope_then_the_subscriber_should_not_be_notified() { 80 | 81 | var notified = false 82 | target.simpleSubscribeTo { 83 | notified = true 84 | } 85 | 86 | target.publish(TestEvent(this), TestScope) 87 | 88 | assertEquals(false, notified, "Subscriber should not have been notified.") 89 | } 90 | 91 | @Test 92 | fun When_subscribed_to_an_event_with_a_key_and_the_proper_event_is_published_then_the_subscriber_should_be_notified() { 93 | 94 | var notified = false 95 | 96 | target.subscribeTo(key = TestEvent.key) { 97 | notified = true 98 | KeepSubscription 99 | } 100 | 101 | target.publish(TestEvent(this)) 102 | 103 | assertEquals(true, notified, "Subscriber was not notified.") 104 | } 105 | 106 | 107 | @Test 108 | fun When_EventBus_has_multiple_subscribers_for_the_same_event_all_should_be_notified_when_that_event_is_fired() { 109 | 110 | val notifications = mutableListOf() 111 | 112 | target.simpleSubscribeTo { 113 | notifications += 0 114 | } 115 | target.simpleSubscribeTo { 116 | notifications += 1 117 | } 118 | 119 | target.publish(TestEvent(this)) 120 | 121 | assertEquals(listOf(0, 1), notifications, "All subscribers should have been notified.") 122 | } 123 | 124 | @Test 125 | fun When_EventBus_has_multiple_subscribers_for_the_same_event_but_different_scopes_only_one_should_be_notified_when_that_event_is_fired() { 126 | 127 | val notifications = mutableListOf() 128 | target.simpleSubscribeTo(TestScope) { 129 | notifications.add(TestScope) 130 | } 131 | target.simpleSubscribeTo(ApplicationScope) { 132 | notifications.add(ApplicationScope) 133 | } 134 | 135 | target.publish(TestEvent(this), TestScope) 136 | 137 | assertEquals( 138 | mutableListOf(TestScope), 139 | notifications, 140 | "Only the subscriber with TestScope should have been notified." 141 | ) 142 | } 143 | 144 | @Test 145 | fun When_subscribed_to_an_event_Then_subscriber_should_be_present_in_EventBus() { 146 | 147 | val subscription = target.simpleSubscribeTo { } 148 | 149 | assertEquals( 150 | expected = listOf(subscription), 151 | actual = target.fetchSubscribersOf(ApplicationScope, TestEvent.key).toList(), 152 | message = "Subscribers should be empty." 153 | ) 154 | 155 | } 156 | 157 | @Test 158 | fun When_unsubscribed_from_event_Then_subscriber_should_not_be_present_in_EventBus() { 159 | 160 | target.simpleSubscribeTo { }.dispose() 161 | 162 | assertEquals( 163 | expected = listOf(), 164 | actual = target.fetchSubscribersOf(ApplicationScope, TestEvent.key).toList(), 165 | message = "Subscribers should be empty." 166 | ) 167 | 168 | } 169 | 170 | // TODO: cancelScope! 171 | @Test 172 | fun When_invoking_callback_causes_exception_subscriber_should_be_cancelled_with_exception() { 173 | val exception = IllegalArgumentException() 174 | val expectedState = DisposedByException(exception) 175 | val subscription = target.simpleSubscribeTo { 176 | throw exception 177 | } 178 | 179 | target.publish(TestEvent(this)) 180 | 181 | assertEquals( 182 | expected = expectedState, 183 | actual = subscription.disposeState, 184 | message = "Subscriber should have been cancelled with exception" 185 | ) 186 | } 187 | 188 | data class TestEvent( 189 | override val emitter: Any, 190 | override val trace: Iterable = listOf() 191 | ) : Event { 192 | 193 | companion object { 194 | val key = TestEvent::class.simpleName!! 195 | } 196 | } 197 | 198 | object TestScope : EventScope 199 | } 200 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=org.hexworks.cobalt 2 | version=2023.1.0-RELEASE 3 | 4 | kotlin.mpp.stability.nowarn=true 5 | kotlin.js.compiler=both 6 | 7 | POM_PACKAGING=jar 8 | POM_URL=https://github.com/Hexworks/cobalt 9 | POM_SCM_URL=https://github.com/Hexworks/cobalt.git 10 | POM_SCM_CONNECTION=git@github.com:Hexworks/cobalt.git 11 | POM_SCM_DEV_CONNECTION=git@github.com:Hexworks/cobalt.git 12 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 13 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 14 | POM_LICENCE_DIST=repo 15 | POM_DEVELOPER_ID=adam-arold 16 | POM_DEVELOPER_NAME=Adam Arold 17 | POM_DEVELOPER_EMAIL=info@hexworks.org 18 | POM_DEVELOPER_ORGANIZATION=Hexworks 19 | POM_DEVELOPER_ORGANIZATION_URL=https://github.com/Hexworks 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hexworks/cobalt/8893267b77c2725b3c458f0691744fbd7854e4f5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./gradlew publish --no-parallel --no-daemon 4 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "cobalt" 2 | 3 | include(":cobalt.core") 4 | --------------------------------------------------------------------------------