diff --git a/.gitignore b/.gitignore
index 8d9ef2c09..a0e81a534 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,27 @@ buildNumber.properties
.mvn/wrapper/maven-wrapper.jar
# LibreOffice lock files (.rgg especially)
-.~lock*
\ No newline at end of file
+.~lock*
+
+# https://github.com/github/gitignore/blob/main/Gradle.gitignore
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
\ No newline at end of file
diff --git a/admin/pom.xml b/admin/pom.xml
index 73039cf7b..230156de3 100644
--- a/admin/pom.xml
+++ b/admin/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/api-client-motorsportreg-test/pom.xml b/api-client-motorsportreg-test/pom.xml
index d1203ff18..2c84d123f 100644
--- a/api-client-motorsportreg-test/pom.xml
+++ b/api-client-motorsportreg-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/api-client-motorsportreg/pom.xml b/api-client-motorsportreg/pom.xml
index b759669e8..b0a91e0e9 100644
--- a/api-client-motorsportreg/pom.xml
+++ b/api-client-motorsportreg/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/buildsrc/buildsrc-kotlin-parent/pom.xml b/buildsrc/buildsrc-kotlin-parent/pom.xml
new file mode 100644
index 000000000..5eef5c04b
--- /dev/null
+++ b/buildsrc/buildsrc-kotlin-parent/pom.xml
@@ -0,0 +1,146 @@
+
+
+ 4.0.0
+
+ tech.coner.trailer
+ parent
+ 0.1.0-SNAPSHOT
+ ../../pom.xml
+
+
+ buildsrc-kotlin-parent
+
+ pom
+
+
+ ../../core
+ ../../core-test
+ ../../datasource-crispy-fish
+ ../../datasource-crispy-fish-test
+ ../../datasource-snoozle
+ ../../api-client-motorsportreg
+ ../../api-client-motorsportreg-test
+ ../../datasource-motorsportreg
+ ../../io
+ ../../io-kodein-di
+ ../../admin
+ ../../io-test
+ ../../presentation/presentation
+ ../../presentation/presentation-text
+ ../../presentation/presentation-all-kodein-di
+ ../../presentation/presentation-json
+ ../../io-kodein-di-test
+ ../../io-testsupport
+ ../../core-kodein-di
+ ../../core-kodein-di-test
+ ../../webapp-competition
+ ../../testutil/assertk-ktor
+ ../../presentation/presentation-all-testsupport-mockk
+ ../../toolkit/konstraints
+ ../../toolkit/presentation/presentation
+ ../../toolkit/presentation/presentation-testsupport
+ ../../toolkit/samples/samples-common
+ ../../toolkit/validation/validation
+ ../../toolkit/validation/validation-testsupport
+ ../../toolkit/toolkit
+ ../../testutil/assertk-arrowkt
+
+
+
+ UTF-8
+ official
+ 1.8
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+ io.arrow-kt
+ arrow-core-jvm
+
+
+ io.arrow-kt
+ arrow-fx-coroutines-jvm
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-test-jvm
+
+
+ org.junit.jupiter
+ junit-jupiter
+
+
+ com.willowtreeapps.assertk
+ assertk-jvm
+
+
+ io.mockk
+ mockk-jvm
+
+
+ app.cash.turbine
+ turbine-jvm
+
+
+
+
+
+ mavenCentral
+ https://repo1.maven.org/maven2/
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+
+
+ maven-failsafe-plugin
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/core-kodein-di-test/pom.xml b/core-kodein-di-test/pom.xml
index 6536a8b97..209d8fe0d 100644
--- a/core-kodein-di-test/pom.xml
+++ b/core-kodein-di-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/core-kodein-di/pom.xml b/core-kodein-di/pom.xml
index c6cda81b8..ed520e126 100644
--- a/core-kodein-di/pom.xml
+++ b/core-kodein-di/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/core-test/pom.xml b/core-test/pom.xml
index 931e8f837..943e0b884 100644
--- a/core-test/pom.xml
+++ b/core-test/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/core/pom.xml b/core/pom.xml
index 510d00867..382c99b2d 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-crispy-fish-test/pom.xml b/datasource-crispy-fish-test/pom.xml
index 598c0359d..05947b890 100644
--- a/datasource-crispy-fish-test/pom.xml
+++ b/datasource-crispy-fish-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-crispy-fish/pom.xml b/datasource-crispy-fish/pom.xml
index 35e5dcb7a..abf504de2 100644
--- a/datasource-crispy-fish/pom.xml
+++ b/datasource-crispy-fish/pom.xml
@@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-motorsportreg/pom.xml b/datasource-motorsportreg/pom.xml
index 0518e08b0..0292e37f4 100644
--- a/datasource-motorsportreg/pom.xml
+++ b/datasource-motorsportreg/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-snoozle/pom.xml b/datasource-snoozle/pom.xml
index 7692f51ba..e4966c200 100644
--- a/datasource-snoozle/pom.xml
+++ b/datasource-snoozle/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-kodein-di-test/pom.xml b/io-kodein-di-test/pom.xml
index ebc1c305d..312c5edcd 100644
--- a/io-kodein-di-test/pom.xml
+++ b/io-kodein-di-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-kodein-di/pom.xml b/io-kodein-di/pom.xml
index 887fa16f7..87d66b25f 100644
--- a/io-kodein-di/pom.xml
+++ b/io-kodein-di/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-test/pom.xml b/io-test/pom.xml
index 552a2d4bd..7c79c8b42 100644
--- a/io-test/pom.xml
+++ b/io-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-testsupport/pom.xml b/io-testsupport/pom.xml
index bd4c00fb9..6199df9bd 100644
--- a/io-testsupport/pom.xml
+++ b/io-testsupport/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io/pom.xml b/io/pom.xml
index 6d447f27c..3fe395aac 100644
--- a/io/pom.xml
+++ b/io/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
tech.coner.trailer
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/pom.xml b/pom.xml
index 12162456b..9e07e7ad1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,50 +10,23 @@
0.1.0-SNAPSHOT
- core
- core-test
- datasource-crispy-fish
- datasource-crispy-fish-test
- datasource-snoozle
- api-client-motorsportreg
- api-client-motorsportreg-test
- datasource-motorsportreg
- io
- io-kodein-di
- admin
- io-test
- presentation/presentation
- presentation/presentation-text
- presentation/presentation-all-kodein-di
- presentation/presentation-json
- io-kodein-di-test
- io-testsupport
- core-kodein-di
- core-kodein-di-test
- webapp-competition
- testutil/assertk-ktor
- presentation/presentation-all-testsupport-mockk
- toolkit/konstraints
- toolkit/presentation/presentation
- toolkit/presentation/presentation-test
- toolkit/presentation/presentation-testsupport
- toolkit/samples/samples-common
- toolkit/validation/validation
- toolkit/validation/validation-testsupport
- toolkit/util
+ buildsrc/buildsrc-kotlin-parent
+ toolkit/samples/dmvapp/dmvapp-gui
+ ${project.version}
UTF-8
17
${maven.compiler.source}
VERBOSE
+ 3.7.1
3.12.1
3.3.1
3.2.3
3.6.0
1.2.2
- 1.9.22
+ 2.0.20
2.0
true
official
@@ -69,6 +42,7 @@
1.4.14
4.2.1
2.4.0
+ 1.2.4
5.10.1
0.28.0
@@ -79,39 +53,11 @@
1.0.0
-
-
- org.jetbrains.kotlin
- kotlin-stdlib-jdk8
-
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-core
-
-
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-test-jvm
-
-
- org.junit.jupiter
- junit-jupiter
-
-
- com.willowtreeapps.assertk
- assertk-jvm
-
-
- io.mockk
- mockk-jvm
-
-
-
org.jetbrains.kotlin
- kotlin-stdlib-jdk8
+ kotlin-stdlib
${kotlin.version}
@@ -209,6 +155,16 @@
mordant-jvm
${mordant.version}
+
+ io.arrow-kt
+ arrow-core-jvm
+ ${arrow.version}
+
+
+ io.arrow-kt
+ arrow-fx-coroutines-jvm
+ ${arrow.version}
+
org.junit.jupiter
@@ -274,26 +230,6 @@
- ${project.basedir}/src/main/kotlin
- ${project.basedir}/src/test/kotlin
-
-
-
- org.jetbrains.kotlin
- kotlin-maven-plugin
-
-
- maven-surefire-plugin
-
-
- maven-failsafe-plugin
-
-
- org.codehaus.mojo
- exec-maven-plugin
-
-
-
@@ -343,6 +279,11 @@
maven-resources-plugin
${maven-resources-plugin.version}
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ ${maven-dependency-plugin.version}
+
org.apache.maven.plugins
maven-compiler-plugin
diff --git a/presentation/presentation-all-kodein-di/pom.xml b/presentation/presentation-all-kodein-di/pom.xml
index 82daec4d0..86f075999 100644
--- a/presentation/presentation-all-kodein-di/pom.xml
+++ b/presentation/presentation-all-kodein-di/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/presentation/presentation-all-testsupport-mockk/pom.xml b/presentation/presentation-all-testsupport-mockk/pom.xml
index 890bd23e8..7840672e9 100644
--- a/presentation/presentation-all-testsupport-mockk/pom.xml
+++ b/presentation/presentation-all-testsupport-mockk/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
presentation-all-testsupport-mockk
diff --git a/presentation/presentation-json/pom.xml b/presentation/presentation-json/pom.xml
index 5947e1a9d..58afb75b0 100644
--- a/presentation/presentation-json/pom.xml
+++ b/presentation/presentation-json/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/presentation/presentation-text/pom.xml b/presentation/presentation-text/pom.xml
index 0eb9938fd..7ab27caff 100644
--- a/presentation/presentation-text/pom.xml
+++ b/presentation/presentation-text/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/presentation/presentation/pom.xml b/presentation/presentation/pom.xml
index e967d4ba9..89306fab7 100644
--- a/presentation/presentation/pom.xml
+++ b/presentation/presentation/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/render-html/pom.xml b/render-html/pom.xml
index 31ec9fe1b..9282dbbf5 100644
--- a/render-html/pom.xml
+++ b/render-html/pom.xml
@@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
diff --git a/testutil/assertk-arrowkt/pom.xml b/testutil/assertk-arrowkt/pom.xml
new file mode 100644
index 000000000..c78b08379
--- /dev/null
+++ b/testutil/assertk-arrowkt/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+ tech.coner.trailer
+ buildsrc-kotlin-parent
+ 0.1.0-SNAPSHOT
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
+
+
+ assertk-arrowkt
+
+
+
+ com.willowtreeapps.assertk
+ assertk-jvm
+ compile
+
+
+ io.arrow-kt
+ arrow-core-jvm
+
+
+
\ No newline at end of file
diff --git a/testutil/assertk-arrowkt/src/main/kotlin/tech/coner/trailer/assertk/arrowkt/ArrowktAssertk.kt b/testutil/assertk-arrowkt/src/main/kotlin/tech/coner/trailer/assertk/arrowkt/ArrowktAssertk.kt
new file mode 100644
index 000000000..cc93594e9
--- /dev/null
+++ b/testutil/assertk-arrowkt/src/main/kotlin/tech/coner/trailer/assertk/arrowkt/ArrowktAssertk.kt
@@ -0,0 +1,30 @@
+package tech.coner.trailer.assertk.arrowkt
+
+import arrow.core.Either
+import arrow.core.getOrElse
+import assertk.Assert
+import assertk.assertions.support.expected
+
+/**
+ * Asserts the Either value is Either.Right. You can pass in an optional lambda to run additional assertions on the right value
+ *
+ * ```
+ * val name = Either.Right("...")
+ * assertThat(name).isRight().hasLength(3)
+ * ```
+ */
+fun Assert>.isRight(): Assert = transform { actual ->
+ actual.getOrElse { expected("to be right") }
+}
+
+/**
+ * Asserts the Either value is Either.Left. You can pass in an optional lambda to run additional assertions on the left value
+ *
+ * ```
+ * val name = Either.Left("...")
+ * assertThat(name).isLeft().hasLength(3)
+ * ```
+ */
+fun Assert>.isLeft(): Assert = transform { actual ->
+ actual.swap().getOrElse { expected("to be left") }
+}
\ No newline at end of file
diff --git a/testutil/assertk-ktor/pom.xml b/testutil/assertk-ktor/pom.xml
index bfcefa303..4d17d351c 100644
--- a/testutil/assertk-ktor/pom.xml
+++ b/testutil/assertk-ktor/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/toolkit/konstraints/pom.xml b/toolkit/konstraints/pom.xml
index dfe701a17..4b77739c1 100644
--- a/toolkit/konstraints/pom.xml
+++ b/toolkit/konstraints/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-konstraints
diff --git a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/data/service/FooService.kt b/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/data/service/FooService.kt
deleted file mode 100644
index 7246d371c..000000000
--- a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/data/service/FooService.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.data.service
-
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-
-interface FooService {
-
- suspend fun create(create: Foo): Result
- suspend fun findById(id: Foo.Id): Result
- suspend fun update(update: Foo): Result
- suspend fun deleteById(id: Foo.Id): Result
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/domain/constraint/FooConstraint.kt b/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/domain/constraint/FooConstraint.kt
deleted file mode 100644
index a1f1868b8..000000000
--- a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/domain/constraint/FooConstraint.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.constraint
-
-import tech.coner.trailer.toolkit.konstraints.CompositeConstraint
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-
-class FooConstraint : CompositeConstraint() {
-
- val valueIsInRange = propertyConstraint(
- property = Foo::id,
- assessFn = {
- when (it.id.value) {
- in 0..4 -> true
- else -> false
- }
- },
- violationMessageFn = { "ID value must be in 0 to 4" }
- )
-
-
- val nameIsLettersOnly = propertyConstraint(
- property = Foo::name,
- assessFn = { foo -> foo.name.all { it.isLetter() } },
- violationMessageFn = { "Name must be only letters" }
- )
-
- val nameMustBeThreeCharacters = propertyConstraint(
- property = Foo::name,
- assessFn = { foo -> foo.name.length == 3 },
- violationMessageFn = { "Name must be three letters long" }
- )
-
- private val vowels = "aeiouy"
- private val consonants = "bcdfghjklmnpqrstvwxz"
- private val namesOtherThanFooPattern = Regex("[$consonants][$vowels][$consonants]")
-
- val namesOtherThanFooMustFollowPatternOfConsonantVowelConsonant = propertyConstraint(
- property = Foo::name,
- assessFn = { foo ->
- when (foo.name) {
- "foo" -> true
- else -> namesOtherThanFooPattern.matches(foo.name)
- }
- },
- violationMessageFn = { "Names other than foo must start with a consonant, followed by a vowel, and end with a consonant" }
- )
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/domain/service/TestableFooService.kt b/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/domain/service/TestableFooService.kt
deleted file mode 100644
index 32ad0fc2a..000000000
--- a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/domain/service/TestableFooService.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.service
-
-import tech.coner.trailer.toolkit.konstraints.Constraint
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.FOO_ID_BAR
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.FOO_ID_BAT
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.FOO_ID_BAZ
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.FOO_ID_FOO
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.exception.AlreadyExistsException
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.exception.NotFoundException
-
-class TestableFooService(private val constraint: Constraint) :
- tech.coner.trailer.toolkit.presentation.testsupport.fooapp.data.service.FooService {
-
- private val map: MutableMap = arrayOf(
- FOO_ID_FOO to "foo",
- FOO_ID_BAR to "bar",
- FOO_ID_BAZ to "baz",
- FOO_ID_BAT to "bat",
- )
- .associate { (idValue, name) -> Foo.Id(idValue).let { it to Foo(it, name) } }
- .toMutableMap()
-
- override suspend fun create(create: Foo): Result {
- if (map.containsKey(create.id)) {
- return Result.failure(AlreadyExistsException())
- }
- return constraint(create)
- .map { map[create.id] = create; create }
- }
-
- override suspend fun findById(id: Foo.Id): Result {
- return map[id]?.let { Result.success(it) }
- ?: Result.failure(NotFoundException())
- }
-
- override suspend fun update(update: Foo): Result {
- return findById(update.id)
- .mapCatching { constraint(update).getOrThrow() }
- .map { map[update.id] = update; update }
- }
-
- override suspend fun deleteById(id: Foo.Id): Result {
- return map.remove(id)
- ?.let { Result.success(it) }
- ?: Result.failure(NotFoundException())
- }
-}
diff --git a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/adapter/FooAdapter.kt b/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/adapter/FooAdapter.kt
deleted file mode 100644
index 601646546..000000000
--- a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/adapter/FooAdapter.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.adapter
-
-import tech.coner.trailer.toolkit.presentation.adapter.LoadableItemAdapter
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.model.FooItemModel
-
-class FooAdapter : LoadableItemAdapter<
- Foo.Id,
- Foo,
- Unit,
- FooItemModel
- >() {
- override val argumentToModelAdapter = null
- override val itemToModelAdapter: (Foo) -> FooItemModel = { FooItemModel(it, this) }
- override val modelToItemAdapter: (FooItemModel) -> Foo = {
- Foo(
- id = it.item.id,
- name = it.name.lowercase()
- )
- }
-
- val modelNameProperty: (Foo) -> String = { it.name.capitalizeFirstChar() }
- val entityNameProperty: (String) -> String = { it.lowercase() }
-
- private fun String.capitalizeFirstChar(): String { // TODO: adapter
- return when (length) {
- 0 -> this
- 1 -> uppercase()
- else -> let { "${it[0].uppercaseChar()}${it.substring(1)}" }
- }
- }
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/model/FooItemModel.kt b/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/model/FooItemModel.kt
deleted file mode 100644
index ed1aa7d76..000000000
--- a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/model/FooItemModel.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.model
-
-import kotlinx.coroutines.runBlocking
-import tech.coner.trailer.toolkit.presentation.model.BaseItemModel
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.constraint.FooConstraint
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.adapter.FooAdapter
-
-class FooItemModel(
- override val initialItem: Foo,
- private val adapter: FooAdapter,
-) : BaseItemModel() {
- override val constraints =
- FooConstraint()
-
- var name: String
- get() = adapter.modelNameProperty(pendingItem)
- set(value) = runBlocking { mutatePendingItem { it.copy(name = adapter.entityNameProperty(value)) } }
-
-}
-
diff --git a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/presenter/FooDetailPresenter.kt b/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/presenter/FooDetailPresenter.kt
deleted file mode 100644
index 664a1376c..000000000
--- a/toolkit/presentation/presentation-test/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/presenter/FooDetailPresenter.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.presenter
-
-import kotlin.coroutines.CoroutineContext
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.delay
-import tech.coner.trailer.toolkit.presentation.model.LoadableModel
-import tech.coner.trailer.toolkit.presentation.presenter.LoadableItemPresenter
-import tech.coner.trailer.toolkit.presentation.state.LoadableItemState
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.adapter.FooAdapter
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.model.FooItemModel
-
-class FooDetailPresenter(
- override val argument: Foo.Id,
- private val service: tech.coner.trailer.toolkit.presentation.testsupport.fooapp.data.service.FooService,
- override val coroutineContext: CoroutineContext
-) : LoadableItemPresenter<
- Foo.Id,
- Foo,
- Unit,
- FooItemModel
- >() {
- override val adapter = FooAdapter()
-
- override suspend fun performLoad(): Result {
- return service.findById(argument)
- .onSuccess { foo ->
- // faking a partial load
- update {
- LoadableItemState(
- LoadableModel.Loading(
- partial = adapter.itemToModelAdapter(
- foo.copy(name = "${foo.name[0]}")
- )
- )
- )
- }
- delay(1.seconds)
- }
- }
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-test/src/test/kotlin/tech/coner/trailer/toolkit/presentation/presenter/FooDetailPresenterTest.kt b/toolkit/presentation/presentation-test/src/test/kotlin/tech/coner/trailer/toolkit/presentation/presenter/FooDetailPresenterTest.kt
deleted file mode 100644
index 73e653c7b..000000000
--- a/toolkit/presentation/presentation-test/src/test/kotlin/tech/coner/trailer/toolkit/presentation/presenter/FooDetailPresenterTest.kt
+++ /dev/null
@@ -1,176 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.presenter
-
-import app.cash.turbine.test
-import assertk.all
-import assertk.assertThat
-import assertk.assertions.isEmpty
-import assertk.assertions.isEqualTo
-import assertk.assertions.isFalse
-import assertk.assertions.isInstanceOf
-import assertk.assertions.isNotNull
-import assertk.assertions.isNull
-import assertk.assertions.isTrue
-import assertk.assertions.length
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
-import tech.coner.trailer.toolkit.presentation.model.cause
-import tech.coner.trailer.toolkit.presentation.model.isDirty
-import tech.coner.trailer.toolkit.presentation.model.isEmpty
-import tech.coner.trailer.toolkit.presentation.model.isLoadFailed
-import tech.coner.trailer.toolkit.presentation.model.isLoaded
-import tech.coner.trailer.toolkit.presentation.model.isLoading
-import tech.coner.trailer.toolkit.presentation.model.isValid
-import tech.coner.trailer.toolkit.presentation.model.item
-import tech.coner.trailer.toolkit.presentation.model.partial
-import tech.coner.trailer.toolkit.presentation.model.violations
-import tech.coner.trailer.toolkit.presentation.state.loadable
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.constraint.FooConstraint
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.FOO_ID_FOO
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.Foo
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.entity.name
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.exception.NotFoundException
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.domain.service.TestableFooService
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.model.name
-import tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.presenter.FooDetailPresenter
-
-class FooDetailPresenterTest {
-
- @Test
- fun itsModelFlowShouldBeAdaptedFromInitialState() = runTest {
- val id = Foo.Id(FOO_ID_FOO)
- val presenter = createPresenter(id)
-
- presenter.stateFlow.test {
- assertThat(expectMostRecentItem())
- .loadable()
- .isEmpty()
- }
- }
-
- @Test
- fun itsModelFlowShouldEmitWhenLoadingAndLoaded() = runTest {
- val id = Foo.Id(FOO_ID_FOO)
- val presenter = createPresenter(id)
-
- presenter.stateFlow.test {
- skipItems(1)
-
- presenter.load()
-
- assertThat(awaitItem())
- .loadable()
- .isLoading()
- .partial()
- .isNull()
- assertThat(awaitItem())
- .loadable()
- .isLoading()
- .partial()
- .isNotNull()
- .item()
- .name()
- .length()
- .isEqualTo(1)
- assertThat(awaitItem())
- .loadable()
- .isLoaded()
- .item()
- .item()
- .name()
- .isEqualTo("foo")
- }
- }
-
- @Test
- fun itsModelFlowShouldEmitWhenLoadingAndLoadFailed() = runTest {
- val id = Foo.Id(Int.MAX_VALUE)
- val presenter = createPresenter(id)
-
- presenter.stateFlow.test {
- skipItems(1)
-
- presenter.load()
-
- assertThat(awaitItem())
- .loadable()
- .isLoading()
- assertThat(awaitItem())
- .loadable()
- .isLoadFailed()
- .cause()
- .isNotNull()
- .isInstanceOf()
- }
- }
-
- @Test
- fun whenItsModelNameChangedValidItsItemModelShouldBeValid() = runTest {
- val id = Foo.Id(FOO_ID_FOO)
- val presenter = createPresenter(id)
- launch { presenter.load() }
- val model = presenter.awaitModelLoadedOrThrow()
-
- model.item.name = "bax"
- model.item.validate()
-
- assertThat(model.item).all {
- name().isEqualTo("Bax")
- isValid().isTrue()
- isDirty().isTrue()
- }
- }
-
- @Test
- fun whenItsModelNameChangedInvalidItsItemModelShouldBeInvalid() = runTest {
- val id = Foo.Id(FOO_ID_FOO)
- val presenter = createPresenter(id)
- launch { presenter.load() }
- val model = presenter.awaitModelLoadedOrThrow()
-
- model.item.name = "boo"
- model.item.validate()
-
- assertThat(model.item).all {
- name().isEqualTo("Boo")
- isValid().isFalse()
- isDirty().isTrue()
- }
- }
-
- @ParameterizedTest
- @ValueSource(strings = ["Bar", "Baz", "Bat", "Cat", "Dat", "Far", "Ber", "Fir", "Nor", "Dur", "Xyr"])
- fun whenItsModelNameChangedValidItsItemModelShouldValidateValid(newName: String) = runTest {
- val id = Foo.Id(FOO_ID_FOO)
- val presenter = createPresenter(id)
- launch { presenter.load() }
- val model = presenter.awaitModelLoadedOrThrow()
-
- with (model.item) {
- name = newName
- validate()
- }
-
- assertThat(model.item).all {
- name().isEqualTo(newName)
- isValid().isTrue()
- violations().isEmpty()
- isDirty().isTrue()
- }
- }
-}
-
-private fun TestScope.createPresenter(argument: Foo.Id): FooDetailPresenter {
- return FooDetailPresenter(
- argument = argument,
- service = TestableFooService(
- constraint = FooConstraint()
- ),
- coroutineContext = coroutineContext + Job() + CoroutineName("FooDetailPresenter")
- )
-}
diff --git a/toolkit/presentation/presentation-test/src/test/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/model/FooModelAssertk.kt b/toolkit/presentation/presentation-test/src/test/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/model/FooModelAssertk.kt
deleted file mode 100644
index 45f4d110f..000000000
--- a/toolkit/presentation/presentation-test/src/test/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/fooapp/presentation/model/FooModelAssertk.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport.fooapp.presentation.model
-
-import assertk.Assert
-import assertk.assertions.prop
-
-fun Assert.name() = prop(FooItemModel::name)
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-testsupport/pom.xml b/toolkit/presentation/presentation-testsupport/pom.xml
index 2419cb405..90670303e 100644
--- a/toolkit/presentation/presentation-testsupport/pom.xml
+++ b/toolkit/presentation/presentation-testsupport/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-presentation-testsupport
diff --git a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModelAssertk.kt b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModelAssertk.kt
index 50bc6549f..a7d2ecde2 100644
--- a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModelAssertk.kt
+++ b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModelAssertk.kt
@@ -2,13 +2,14 @@ package tech.coner.trailer.toolkit.presentation.model
import assertk.Assert
import assertk.assertions.prop
+import tech.coner.trailer.toolkit.validation.Feedback
-fun Assert>.item() = prop(ItemModel::item)
+fun > Assert>.item() = prop(ItemModel::item)
-fun Assert>.pendingItem() = prop(ItemModel::pendingItem)
+fun > Assert>.pendingItem() = prop(ItemModel::pendingItem)
-fun Assert>.violations() = prop(ItemModel::pendingItemValidation)
+fun > Assert>.pendingItemValidation() = prop(ItemModel::pendingItemValidation)
-fun Assert>.isValid() = prop(ItemModel::isPendingItemValid)
+fun > Assert>.isValid() = prop(ItemModel::isPendingItemValid)
-fun Assert>.isDirty() = prop(ItemModel::isPendingItemDirty)
\ No newline at end of file
+fun > Assert>.isDirty() = prop(ItemModel::isPendingItemDirty)
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModelAssertk.kt b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModelAssertk.kt
index 9642f6798..9e499872b 100644
--- a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModelAssertk.kt
+++ b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModelAssertk.kt
@@ -3,14 +3,14 @@ package tech.coner.trailer.toolkit.presentation.model
import assertk.Assert
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
+import tech.coner.trailer.toolkit.validation.Feedback
-fun - > Assert>.isEmpty() = isInstanceOf>()
+fun , FEEDBACK : Feedback
- > Assert>.isEmpty() = isInstanceOf>()
-fun
- > Assert>.isLoading() = isInstanceOf>()
-fun
- > Assert>.partial() = prop(LoadableModel.Loading
- ::partial)
+fun , FEEDBACK : Feedback
- > Assert>.isLoading() = isInstanceOf>()
+fun , FEEDBACK : Feedback
- > Assert>.partial() = prop(Loadable.Loading::partial)
-fun
- > Assert>.isLoaded() = isInstanceOf>()
-fun
- > Assert>.item() = prop(LoadableModel.Loaded
- ::item)
+fun , FEEDBACK : Feedback
- > Assert>.isLoaded() = isInstanceOf>()
+fun , FEEDBACK : Feedback
- > Assert>.either() = prop(Loadable.Loaded::either)
-fun
- > Assert>.isLoadFailed() = isInstanceOf>()
-fun
- > Assert>.cause() = prop(LoadableModel.LoadFailed
- ::cause)
+// TODO: convenient API for asserting loaded either success or failure
diff --git a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/ItemModelStateAssertk.kt b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/ItemModelStateAssertk.kt
new file mode 100644
index 000000000..5ef47079c
--- /dev/null
+++ b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/ItemModelStateAssertk.kt
@@ -0,0 +1,7 @@
+package tech.coner.trailer.toolkit.presentation.state
+
+import assertk.Assert
+import assertk.assertions.prop
+import tech.coner.trailer.toolkit.validation.Feedback
+
+fun
- > Assert>.itemModel() = prop(ItemModelState
- ::itemModel)
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemStateAssertk.kt b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemStateAssertk.kt
index 52ff64963..528e2a6ff 100644
--- a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemStateAssertk.kt
+++ b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemStateAssertk.kt
@@ -3,5 +3,11 @@ package tech.coner.trailer.toolkit.presentation.state
import assertk.Assert
import assertk.assertions.prop
import tech.coner.trailer.toolkit.presentation.model.ItemModel
+import tech.coner.trailer.toolkit.validation.Feedback
-fun
- > Assert>.loadable() = prop(LoadableItemState
- ::loadable)
\ No newline at end of file
+fun , FAILURE, ITEM, ITEM_MODEL : ItemModel
- , FEEDBACK : Feedback
- >
+ Assert.loadable() = prop(LoadableState::loadable)
+
+fun , FAILURE, ITEM, ITEM_MODEL : ItemModel
- , FEEDBACK : Feedback
- , PROPERTY>
+ Assert>.value()
+ = prop(MutableLoadedPropertyDelegate::value)
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/StatefulPresenterAssertk.kt b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/StatefulPresenterAssertk.kt
new file mode 100644
index 000000000..9aa96a22a
--- /dev/null
+++ b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/StatefulPresenterAssertk.kt
@@ -0,0 +1,7 @@
+package tech.coner.trailer.toolkit.presentation.state
+
+import assertk.Assert
+import assertk.assertions.prop
+import tech.coner.trailer.toolkit.presentation.presenter.StatefulPresenter
+
+fun
Assert>.state() = prop(StatefulPresenter::state)
\ No newline at end of file
diff --git a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/LoadableItemPresenterAssertk.kt b/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/LoadableItemPresenterAssertk.kt
deleted file mode 100644
index 9dd5acf0f..000000000
--- a/toolkit/presentation/presentation-testsupport/src/main/kotlin/tech/coner/trailer/toolkit/presentation/testsupport/LoadableItemPresenterAssertk.kt
+++ /dev/null
@@ -1,2 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.testsupport
-
diff --git a/toolkit/presentation/presentation/pom.xml b/toolkit/presentation/presentation/pom.xml
index 6c412d6d5..b845a5201 100644
--- a/toolkit/presentation/presentation/pom.xml
+++ b/toolkit/presentation/presentation/pom.xml
@@ -5,15 +5,20 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-presentation
+
+ tech.coner.trailer
+ toolkit
+ 0.1.0-SNAPSHOT
+
tech.coner.trailer
toolkit-validation
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/EntityModelAdapter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/EntityModelAdapter.kt
new file mode 100644
index 000000000..296c650f6
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/EntityModelAdapter.kt
@@ -0,0 +1,8 @@
+package tech.coner.trailer.toolkit.presentation.adapter
+
+abstract class EntityModelAdapter- {
+
+ abstract val entityToModelAdapter: (ITEM) -> MODEL
+ abstract val modelToEntityAdapter: (MODEL) -> ITEM
+}
+
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt
deleted file mode 100644
index fb77dc83f..000000000
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.adapter
-
-import tech.coner.trailer.toolkit.presentation.model.ItemModel
-import tech.coner.trailer.toolkit.validation.Feedback
-
-abstract class LoadableItemAdapter
-
- where ITEM_MODEL : ItemModel
- {
-
- abstract val itemToModelAdapter: (ITEM) -> ITEM_MODEL
- abstract val modelToItemAdapter: (ITEM_MODEL) -> ITEM
-}
-
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt
index 8f011ba30..584cf5c71 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt
@@ -1,15 +1,22 @@
package tech.coner.trailer.toolkit.presentation.model
+import arrow.core.Either
+import arrow.core.raise.either
+import arrow.core.raise.ensure
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import tech.coner.trailer.toolkit.validation.Feedback
-import tech.coner.trailer.toolkit.validation.ValidationResult
+import tech.coner.trailer.toolkit.validation.ValidationOutcome
import tech.coner.trailer.toolkit.validation.Validator
abstract class BaseItemModel
-
: ItemModel
-
- where VALIDATOR_FEEDBACK : Feedback {
+ where VALIDATOR_FEEDBACK : Feedback
- {
abstract val validator: Validator
abstract val validatorContext: VALIDATOR_CONTEXT
@@ -24,43 +31,70 @@ abstract class BaseItemModel
-
final override val pendingItemFlow by lazy { _pendingItemFlow.asStateFlow() }
final override var pendingItem: ITEM
get() = pendingItemFlow.value
- set(value) = mutatePendingItem { value }
+ set(value) = updatePendingItem { value }
private val _pendingItemValidationFlow by lazy {
- MutableStateFlow>(
- ValidationResult(emptyMap())
- )
+ MutableStateFlow>(ValidationOutcome(emptyList()))
+ }
+ final override val pendingItemValidationFlow: StateFlow> by lazy {
+ _pendingItemValidationFlow.asStateFlow()
}
- final override val pendingItemValidationFlow by lazy { _pendingItemValidationFlow.asStateFlow() }
final override val pendingItemValidation get() = _pendingItemValidationFlow.value
- final override fun mutatePendingItem(forceValidate: Boolean?, mutatePendingItemFn: (ITEM) -> ITEM) {
- _pendingItemFlow.update { pending -> mutatePendingItemFn(pending) }
- if (forceValidate == true) {
+ final override fun updatePendingItem(forceValidate: Boolean?, updateFn: (ITEM) -> ITEM) {
+ val pendingItemHadFeedback = _pendingItemValidationFlow.value.feedback.isNotEmpty()
+ _pendingItemFlow.update { pending ->
+ updateFn(pending)
+ }
+ if (forceValidate == true || pendingItemHadFeedback) {
validate()
}
}
final override val isPendingItemValid
get() = _pendingItemValidationFlow.value.isValid
+ final override val isPendingItemValidFlow by lazy {
+ _pendingItemValidationFlow.map { it.isValid }
+ }
final override val isPendingItemDirty
get() = item != pendingItem
+ final override val isPendingItemDirtyFlow: Flow by lazy {
+ pendingItemFlow
+ .map { item != it }
+ }
- final override fun validate(): ValidationResult
- {
+ override val canReset: Boolean get() = pendingItem != item || _pendingItemValidationFlow.value.feedback.isNotEmpty()
+ override val canResetFlow: Flow by lazy {
+ combine(
+ _pendingItemFlow,
+ _pendingItemValidationFlow,
+ ) { pendingItem, pendingItemValidation ->
+ pendingItem != item || pendingItemValidation.feedback.isNotEmpty() || item != initialItem
+ }
+ }
+
+ final override fun validate(): ValidationOutcome
- {
return validator(validatorContext, pendingItem)
.also { _pendingItemValidationFlow.value = it }
}
- override fun commit(requireValid: Boolean): CommitOutcome
- {
+
+
+ override fun commit(requireValid: Boolean): Either, ITEM> = either {
val validationResult = validate()
if (requireValid) {
- if (!validationResult.isValid) {
- return CommitOutcome.Failure(pendingItem, validationResult)
+ ensure(validationResult.isValid) {
+ validationResult
}
}
- return pendingItem
+ pendingItem
.also { _itemFlow.value = it }
- .let { CommitOutcome.Success(it, validationResult) }
}
-}
\ No newline at end of file
+
+ override fun reset() {
+ _itemFlow.value = initialItem
+ _pendingItemFlow.value = initialItem
+ _pendingItemValidationFlow.value = ValidationOutcome(emptyList())
+ }
+}
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt
deleted file mode 100644
index 3d86761b4..000000000
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.model
-
-import tech.coner.trailer.toolkit.validation.Feedback
-import tech.coner.trailer.toolkit.validation.ValidationResult
-
-sealed class CommitOutcome
- {
-
- abstract val item: ITEM
- abstract val feedback: ValidationResult
- ?
-
- data class Success
- (
- override val item: ITEM,
- override val feedback: ValidationResult
- ?
- ) : CommitOutcome
- ()
-
- data class Failure
- (
- override val item: ITEM,
- override val feedback: ValidationResult
-
- ) : CommitOutcome
- ()
-
- fun onSuccess(fn: (Success
- ) -> Unit): CommitOutcome
- {
- if (this is Success
- ) fn(this)
- return this
- }
-
- fun onFailure(fn: (Failure
- ) -> Unit): CommitOutcome
- {
- if (this is Failure
- ) fn(this)
- return this
- }
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt
index a3b75ec46..4673fec38 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt
@@ -1,27 +1,35 @@
package tech.coner.trailer.toolkit.presentation.model
+import arrow.core.Either
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import tech.coner.trailer.toolkit.validation.Feedback
-import tech.coner.trailer.toolkit.validation.ValidationResult
+import tech.coner.trailer.toolkit.validation.ValidationOutcome
-interface ItemModel
- : Model {
- val itemFlow: StateFlow
-
+interface ItemModel
- > : Model {
val item: ITEM
+ val itemFlow: StateFlow
-
- val pendingItemFlow: StateFlow
-
var pendingItem: ITEM
+ val pendingItemFlow: StateFlow
-
val isPendingItemDirty: Boolean
+ val isPendingItemDirtyFlow: Flow
- val pendingItemValidationFlow: Flow>
- val pendingItemValidation: ValidationResult
-
+ val pendingItemValidation: ValidationOutcome
-
+ val pendingItemValidationFlow: StateFlow>
val isPendingItemValid: Boolean
+ val isPendingItemValidFlow: Flow
+
+ val canReset: Boolean
+ val canResetFlow: Flow
+
+ fun updatePendingItem(forceValidate: Boolean? = null, updateFn: (ITEM) -> ITEM)
- fun mutatePendingItem(forceValidate: Boolean? = null, mutatePendingItemFn: (ITEM) -> ITEM)
+ fun validate(): ValidationOutcome
-
- fun validate(): ValidationResult
-
+ fun commit(requireValid: Boolean = true): Either, ITEM>
- fun commit(requireValid: Boolean = true): CommitOutcome
-
+ fun reset()
}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Loadable.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Loadable.kt
new file mode 100644
index 000000000..6d666a7eb
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Loadable.kt
@@ -0,0 +1,73 @@
+package tech.coner.trailer.toolkit.presentation.model
+
+import arrow.core.Either
+import tech.coner.trailer.toolkit.validation.Feedback
+
+sealed interface Loadable, FEEDBACK : Feedback
- >
+ : Model {
+
+ /**
+ * Initial model corresponding to the presenter having initial state,
+ * prior to starting to load anything, or if it was fully reset.
+ */
+ class Empty, FEEDBACK : Feedback
- >
+ : Loadable {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return javaClass.hashCode()
+ }
+ }
+
+ /**
+ * Model indicates the item is loading.
+ *
+ * @property partial Partial model representing incomplete loading
+ * state. For long or complex load operations, use to convey details
+ * of loading in progress. Adapters may optionally specify this value.
+ * It may be helpful to implement the partial model with a different
+ * type than the loaded item.
+ */
+ data class Loading, FEEDBACK : Feedback
- >(
+ val partial: ITEM_MODEL? = null
+ ) : Loadable
+
+ /**
+ * Indicates a load operation completed, either in failure or success.
+ */
+ data class Loaded, FEEDBACK : Feedback
- >(
+ val either: Either
+ )
+ : Loadable
+
+ data class FailedExceptionally, FEEDBACK : Feedback
- >(
+ val throwable: Throwable
+ )
+ : Loadable
+}
+
+inline fun , FEEDBACK : Feedback
- > Loadable.letLoadedSuccess(
+ block: (ITEM_MODEL) -> R
+): R? {
+ return when (this) {
+ is Loadable.Loaded -> either.getOrNull()?.let(block)
+ is Loadable.Empty,
+ is Loadable.Loading,
+ is Loadable.FailedExceptionally -> null
+ }
+}
+
+inline fun , FEEDBACK : Feedback
- > Loadable.whenLoadedSuccess(
+ block: (ITEM_MODEL) -> Unit
+) {
+ when (this) {
+ is Loadable.Loaded -> either.getOrNull()?.also(block)
+ is Loadable.Empty,
+ is Loadable.Loading,
+ is Loadable.FailedExceptionally -> { /* no-op */ }
+ }
+}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt
deleted file mode 100644
index 2262b47e2..000000000
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.model
-
-import tech.coner.trailer.toolkit.validation.Feedback
-
-sealed class LoadableModel
- , FEEDBACK : Feedback>
- : Model {
-
- /**
- * Initial model corresponding to the presenter having initial state,
- * prior to starting to load anything, or if it was fully reset.
- */
- class Empty
- , FEEDBACK : Feedback> : LoadableModel
- () {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- return true
- }
-
- override fun hashCode(): Int {
- return javaClass.hashCode()
- }
- }
-
- /**
- * Model indicates the item is loading.
- *
- * @property partial Partial model representing incomplete loading
- * state. For long or complex load operations, use to convey details
- * of loading in progress. Adapters may optionally specify this value.
- * It may be helpful to implement the partial model with a different
- * type than the loaded item.
- */
- data class Loading
- , FEEDBACK : Feedback>(
- val partial: ITEM_MODEL? = null
- ) : LoadableModel
- ()
-
- /**
- * Model indicates the item has loaded.
- *
- * @property item the item resulting from the load operation.
- */
- data class Loaded
- , FEEDBACK : Feedback>(
- val item: ITEM_MODEL
- ) : LoadableModel
- ()
-
- /**
- * Model indicates the load operation failed.
- *
- * @property cause the cause of the failure, if known
- */
- data class LoadFailed
- , FEEDBACK : Feedback>(
- val cause: Throwable
- ) : LoadableModel
- ()
-
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/ItemModelPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/ItemModelPresenter.kt
new file mode 100644
index 000000000..222d6cd0c
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/ItemModelPresenter.kt
@@ -0,0 +1,10 @@
+package tech.coner.trailer.toolkit.presentation.presenter
+
+interface ItemModelPresenter {
+
+ suspend fun reset()
+
+ suspend fun commit()
+
+ suspend fun validate()
+}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt
deleted file mode 100644
index 0afe3cec3..000000000
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.presenter
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import tech.coner.trailer.toolkit.presentation.adapter.LoadableItemAdapter
-import tech.coner.trailer.toolkit.presentation.model.ItemModel
-import tech.coner.trailer.toolkit.presentation.model.LoadableModel
-import tech.coner.trailer.toolkit.presentation.model.Model
-import tech.coner.trailer.toolkit.presentation.state.LoadableItemState
-import tech.coner.trailer.toolkit.validation.Feedback
-
-abstract class LoadableItemPresenter
- (
- override val initialState: LoadableItemState
- = LoadableItemState(LoadableModel.Empty())
-)
- : SecondDraftPresenter<
- LoadableItemState
-
- >(), CoroutineScope
- where ITEM_MODEL : ItemModel
- {
-
- protected abstract val adapter: LoadableItemAdapter
-
-
- suspend fun load() {
- update { old -> old.copy(LoadableModel.Loading()) }
- performLoad()
- .onSuccess { loaded -> update { old -> old.copy(LoadableModel.Loaded(adapter.itemToModelAdapter(loaded))) } }
- .onFailure { failed -> update { old -> old.copy(LoadableModel.LoadFailed(failed)) } }
- }
-
- protected abstract suspend fun performLoad(): Result
-
-
- suspend fun awaitModelLoadedOrFailed(): LoadableModel
- {
- return stateFlow
- .map { it.loadable }
- .first {
- when (it) {
- is LoadableModel.Loaded, is LoadableModel.LoadFailed -> true
- else -> false
- }
- }
- }
-
- suspend fun awaitModelLoadedOrThrow(): LoadableModel.Loaded
- {
- return stateFlow
- .map { it.loadable }
- .first {
- when (it) {
- is LoadableModel.Loaded -> true
- is LoadableModel.LoadFailed -> throw it.cause
- else -> false
- }
- } as LoadableModel.Loaded
-
- }
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadablePresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadablePresenter.kt
new file mode 100644
index 000000000..46550e701
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadablePresenter.kt
@@ -0,0 +1,9 @@
+package tech.coner.trailer.toolkit.presentation.presenter
+
+import arrow.core.Either
+import kotlinx.coroutines.Deferred
+
+interface LoadablePresenter {
+
+ suspend fun load(): Deferred>>
+}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/PresenterCoroutineScope.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/PresenterCoroutineScope.kt
index 18107f767..6853ff59b 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/PresenterCoroutineScope.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/PresenterCoroutineScope.kt
@@ -1,5 +1,9 @@
package tech.coner.trailer.toolkit.presentation.presenter
import kotlinx.coroutines.CoroutineScope
+import kotlin.coroutines.CoroutineContext
-class PresenterCoroutineScope(coroutineScope: CoroutineScope) : CoroutineScope by coroutineScope
+class PresenterCoroutineScope(coroutineScope: CoroutineScope) : CoroutineScope by coroutineScope {
+
+ constructor(coroutineContext: CoroutineContext) : this(CoroutineScope(coroutineContext))
+}
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt
deleted file mode 100644
index f161a3a88..000000000
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.presenter
-
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import tech.coner.trailer.toolkit.presentation.model.Model
-import tech.coner.trailer.toolkit.presentation.state.State
-
-abstract class SecondDraftPresenter
- where STATE : State {
-
- abstract val initialState: STATE
- private val _stateMutex = Mutex()
- private val _stateFlow: MutableStateFlow by lazy { MutableStateFlow(initialState) }
- val stateFlow: StateFlow by lazy { _stateFlow.asStateFlow() }
- val state: STATE
- get() = runBlocking {
- _stateMutex.withLock {
- _stateFlow.value
- }
- }
-
- protected suspend fun update(reduceFn: (old: STATE) -> STATE) {
- _stateMutex.withLock {
- _stateFlow.update(reduceFn)
- }
- }
-
- interface WithArgument {
-
- val argument: ARGUMENT
- val argumentModel: ARGUMENT_MODEL
-
- }
-}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/StatefulPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/StatefulPresenter.kt
new file mode 100644
index 000000000..93f7420f7
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/StatefulPresenter.kt
@@ -0,0 +1,9 @@
+package tech.coner.trailer.toolkit.presentation.presenter
+
+import kotlinx.coroutines.flow.StateFlow
+
+interface StatefulPresenter
{
+
+ val state: S
+ val stateFlow: StateFlow
+}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/ItemModelState.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/ItemModelState.kt
new file mode 100644
index 000000000..ba9966cd6
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/ItemModelState.kt
@@ -0,0 +1,52 @@
+package tech.coner.trailer.toolkit.presentation.state
+
+import kotlin.reflect.KProperty1
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import tech.coner.trailer.toolkit.presentation.model.ItemModel
+import tech.coner.trailer.toolkit.validation.Feedback
+import tech.coner.trailer.toolkit.validation.ValidationOutcome
+
+interface ItemModelState- > : State {
+ val itemModel: ItemModel
-
+}
+
+class MutableItemModelPropertyDelegate
+, ITEM, FEEDBACK : Feedback
- , PROPERTY>(
+ private val stateContainer: StateContainer,
+ private val getFn: ITEM.() -> PROPERTY,
+ private val updateFn: ITEM.(PROPERTY) -> ITEM,
+ private val property: KProperty1
-
+) {
+ var value: PROPERTY
+ get() = stateContainer.state.itemModel.pendingItem.let(getFn)
+ set(value) {
+ stateContainer.state.itemModel.updatePendingItem { updateFn(it, value) }
+ }
+ val valueFlow: Flow = stateContainer.stateFlow
+ .flatMapLatest { it.itemModel.pendingItemFlow }
+ .map(getFn)
+
+ val feedback: List
+ get() = stateContainer.state.itemModel.pendingItemValidation.feedbackByProperty[property]
+ ?: emptyList()
+ val feedbackFlow: Flow
> = stateContainer.stateFlow
+ .flatMapLatest {
+ flow {
+ emit(ValidationOutcome(emptyList()))
+ it.itemModel.pendingItemValidationFlow.collect(this)
+ }
+ }
+ .map { it.feedbackByProperty[property] ?: emptyList() }
+}
+
+fun , ITEM, FEEDBACK : Feedback- , PROPERTY>
+ StateContainer.mutableItemModelProperty(
+ getFn: ITEM.() -> PROPERTY,
+ updateFn: ITEM.(PROPERTY) -> ITEM,
+ property: KProperty1
-
+): MutableItemModelPropertyDelegate {
+ return MutableItemModelPropertyDelegate(this, getFn, updateFn, property)
+}
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt
deleted file mode 100644
index f9a9b8b81..000000000
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package tech.coner.trailer.toolkit.presentation.state
-
-import tech.coner.trailer.toolkit.presentation.model.ItemModel
-import tech.coner.trailer.toolkit.presentation.model.LoadableModel
-import tech.coner.trailer.toolkit.validation.Feedback
-
-data class LoadableItemState
- , FEEDBACK : Feedback>(
- val loadable: LoadableModel
-
-) : State
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableState.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableState.kt
new file mode 100644
index 000000000..050e370b1
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableState.kt
@@ -0,0 +1,56 @@
+package tech.coner.trailer.toolkit.presentation.state
+
+import kotlinx.coroutines.flow.*
+import tech.coner.trailer.toolkit.presentation.model.ItemModel
+import tech.coner.trailer.toolkit.presentation.model.Loadable
+import tech.coner.trailer.toolkit.presentation.model.letLoadedSuccess
+import tech.coner.trailer.toolkit.presentation.model.whenLoadedSuccess
+import tech.coner.trailer.toolkit.validation.Feedback
+import kotlin.reflect.KProperty1
+
+interface LoadableState, FEEDBACK : Feedback
- >: State {
+ val loadable: Loadable
+}
+
+class MutableLoadedPropertyDelegate
+ , FAILURE, ITEM, ITEM_MODEL : ItemModel
- , FEEDBACK : Feedback
- , PROPERTY>(
+ private val stateContainer: StateContainer,
+ private val getFn: ITEM.() -> PROPERTY,
+ private val updateFn: ITEM.(PROPERTY) -> ITEM,
+ private val property: KProperty1
-
+) {
+ var value: PROPERTY
+ get() = stateContainer.state.loadable.letLoadedSuccess { it.pendingItem?.let(getFn) }
+ // let's see if this causes race conditions in the sample app
+ // might need to add an extra property for null->default value
+ ?: throw IllegalStateException("Accessing successfully loaded state but not successfully loaded")
+ set(value) {
+ stateContainer.state.loadable.whenLoadedSuccess { loaded ->
+ loaded.updatePendingItem { updateFn(it, value) }
+ }
+ }
+ val valueFlow: Flow = stateContainer.stateFlow
+ .map { if (it.loadable is Loadable.Loaded) it.loadable as Loadable.Loaded else null }
+ .flatMapLatest { it?.either?.getOrNull()?.pendingItemFlow ?: flowOf(null) }
+ .map { it?.let(getFn) }
+
+ val feedback: List
+ get() = stateContainer.state.loadable.letLoadedSuccess { it.pendingItemValidation.feedbackByProperty[property] }
+ ?: emptyList()
+ val feedbackFlow: Flow
> = stateContainer.stateFlow
+ .map { if (it.loadable is Loadable.Loaded) it.loadable as Loadable.Loaded else null }
+ .flatMapLatest {
+ it?.letLoadedSuccess { itemModel -> flowOf(itemModel.pendingItemValidation.feedbackByProperty[property]) }
+ ?: flowOf(emptyList())
+ }
+ .map { it ?: emptyList() }
+}
+
+fun , FAILURE, ITEM, ITEM_MODEL : ItemModel- , FEEDBACK : Feedback
- , PROPERTY>
+ StateContainer.mutableLoadedProperty(
+ getFn: ITEM.() -> PROPERTY,
+ updateFn: ITEM.(PROPERTY) -> ITEM,
+ property: KProperty1
-
+): MutableLoadedPropertyDelegate {
+ return MutableLoadedPropertyDelegate(this, getFn, updateFn, property)
+}
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/StateContainer.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/StateContainer.kt
new file mode 100644
index 000000000..8fb0bfb4d
--- /dev/null
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/StateContainer.kt
@@ -0,0 +1,84 @@
+package tech.coner.trailer.toolkit.presentation.state
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import tech.coner.trailer.toolkit.presentation.state.StateContainer.ReducedStateInspector
+
+class StateContainer(
+ val initialState: STATE,
+ private val isReducedStateLegal: ReducedStateInspector = ReducedStateInspector { _, _ -> true }
+) {
+
+ private val _stateMutex = Mutex()
+ private val _stateFlow: MutableStateFlow by lazy { MutableStateFlow(initialState) }
+ val stateFlow: StateFlow by lazy { _stateFlow.asStateFlow() }
+ val state: STATE
+ get() = runBlocking {
+ _stateMutex.withLock {
+ _stateFlow.value
+ }
+ }
+
+ suspend fun update(reduceFn: (old: STATE) -> STATE) {
+ _stateMutex.withLock {
+ val oldState = _stateFlow.value
+ val reducedState = reduceFn(oldState)
+ check(isReducedStateLegal(oldState = oldState, reducedState = reducedState))
+ _stateFlow.emit(reducedState)
+ }
+ }
+
+ /**
+ * Inspects reduced state for legality.
+ *
+ * StateContainer will invoke this interface with the old and reduced state when updating to check it for legality.
+ *
+ * Use this as a last-ditch safety measure to prevent your app entering an illegal state. Be prepared to handle
+ * the IllegalStateException or else your app may crash.
+ *
+ * This is not the place to perform expensive validations. This will be invoked on every call to
+ * `StateContainer.update` with a mutex lock. Keep it simple, light, fast, and safe.
+ */
+ fun interface ReducedStateInspector {
+
+ operator fun invoke(oldState: STATE, reducedState: STATE): Boolean
+ }
+
+ open inner class StatePropertyContainer(
+ private val getValue: STATE.() -> PROPERTY
+ ) {
+ val immutableValue: PROPERTY
+ get() = state.getValue()
+ val flow: Flow = stateFlow.map(getValue)
+ }
+
+ fun