Compare commits

...

180 Commits

Author SHA1 Message Date
ca8ae4cd72 improve ktor server part 2022-04-19 11:16:04 +06:00
53d35d74b3 start 0.9.22 2022-04-19 11:14:50 +06:00
49c139e235 Merge pull request #144 from InsanusMokrassar/0.9.21
0.9.21
2022-04-17 08:56:59 +06:00
caf9c821f3 fixes in AbstractExposedWriteCRUDRepo 2022-04-15 00:49:32 +06:00
ca4c6db96f start 0.9.21 2022-04-15 00:44:28 +06:00
6b2298c752 Merge pull request #143 from InsanusMokrassar/0.9.20
0.9.20
2022-04-13 16:02:12 +06:00
a1bf43def9 upfixes 2022-04-13 16:01:13 +06:00
15e9254e00 fixes in OneToManyAndroidRepo and adding of CursorIterator 2022-04-13 15:54:48 +06:00
afe5a72c6f small fix of docs 2022-04-12 15:32:25 +06:00
750a8b9ecf start 0.9.20 2022-04-12 15:13:25 +06:00
27fc3f93e0 Merge pull request #142 from InsanusMokrassar/0.9.19
0.9.19
2022-04-08 01:16:13 +06:00
8166d4b99b Update CHANGELOG.md 2022-04-08 01:16:01 +06:00
b61d2ae2eb fixes in versions repo 2022-04-07 15:41:08 +06:00
4790fe0aea get back either serializer fun 2022-04-07 09:59:58 +06:00
bc37b11cee downgrade kotlin 2022-04-07 09:56:23 +06:00
223fed910f fix for column name of table version in exposed standard versions repo proxy 2022-04-07 00:43:55 +06:00
b85ab7b061 update coroutines 2022-04-06 11:47:06 +06:00
888dc299c9 update kotlin 2022-04-04 10:06:57 +06:00
e113dc28ed start 0.9.19 2022-04-04 10:05:56 +06:00
31e55d2307 Merge pull request #141 from InsanusMokrassar/0.9.18
0.9.18
2022-03-31 20:38:43 +06:00
e90645f248 Element#onActionOutside 2022-03-31 14:48:15 +06:00
4bb7ba2571 start 0.9.18 2022-03-31 14:31:04 +06:00
8d31c25bf8 Merge pull request #140 from InsanusMokrassar/0.9.17
0.9.17
2022-03-27 17:47:11 +06:00
c7ee1c28b2 fix of docs for DefaultStatesManager 2022-03-27 17:43:01 +06:00
99b09c8b28 add conflicts resolver for default states managers 2022-03-27 17:15:23 +06:00
a328c4425a Update CHANGELOG.md 2022-03-27 13:57:08 +06:00
c0f61ca896 Update libs.versions.toml 2022-03-26 19:11:44 +06:00
86e70c0961 js visibility of objects extensions 2022-03-26 11:01:40 +06:00
d87a3a039f start 0.9.17 2022-03-26 10:57:50 +06:00
6279a2c40a Merge pull request #139 from InsanusMokrassar/0.9.16
0.9.16
2022-03-18 00:10:25 +06:00
f377ebea88 add renderComposableAndLinkToRoot 2022-03-17 19:16:31 +06:00
7373fef964 add js onRemoved for nodes and different extensions for Composition 2022-03-17 14:56:14 +06:00
41ef86dbda start 0.9.16 2022-03-17 14:56:14 +06:00
7bc2b2336d Merge pull request #138 from InsanusMokrassar/0.9.15
0.9.15
2022-03-16 19:47:28 +06:00
0a615e6d78 cancel jobs of completed states in DefaultStatesMachine 2022-03-16 19:35:14 +06:00
8f928e16e1 FSM updates 2022-03-16 18:40:21 +06:00
72bc8da1e7 start 0.9.15 2022-03-16 17:59:29 +06:00
51e349a5db Merge pull request #137 from InsanusMokrassar/0.9.14
0.9.14 (upfixes)
2022-03-15 16:32:24 +06:00
3cbb19ba2c fixes and versions updates 2022-03-15 16:30:32 +06:00
ae3a5bf45d Merge pull request #136 from InsanusMokrassar/0.9.14
0.9.14
2022-03-15 16:22:35 +06:00
9c667f4b78 several fixes in actual temporal upload of js client 2022-03-15 15:37:41 +06:00
21195e1bcb temporal files uploading functionality 2022-03-15 15:33:45 +06:00
03117ac565 start 0.9.14 2022-03-15 15:31:58 +06:00
d13fbdf176 Merge pull request #135 from InsanusMokrassar/0.9.13
0.9.13
2022-03-13 17:14:23 +06:00
7cecc0e0b6 Update CHANGELOG.md 2022-03-13 14:08:01 +06:00
203e781f5d Update libs.versions.toml 2022-03-13 14:06:16 +06:00
3eb6cd77cd start 0.9.13 2022-03-13 14:05:24 +06:00
51855b2405 Merge pull request #134 from InsanusMokrassar/0.9.12
0.9.12
2022-03-12 01:46:23 +06:00
00cc26d874 fix in publication scripts 2022-03-12 01:38:13 +06:00
02520636ad hotfix in selectFile 2022-03-12 01:28:53 +06:00
76813fae8e hotfix in selectFile 2022-03-12 01:28:41 +06:00
4693483c2b rename selectFile with result MPPFile to selectFileOrThrow 2022-03-12 01:12:17 +06:00
6dbd12df59 fill changelog and rename selectOptionalFile to selectFileOrNull 2022-03-12 01:03:41 +06:00
9e84dc5031 add a set of tools for JS and Web Compose 2022-03-12 00:55:03 +06:00
8f790360bc start 0.9.12 2022-03-12 00:34:22 +06:00
808375cea6 Merge pull request #133 from InsanusMokrassar/0.9.11
0.9.11
2022-03-11 13:33:49 +06:00
d9c15db9de add compose submodule and add several functions in top of composition 2022-03-10 17:47:47 +06:00
cf2be8ed43 migration onto toml 2022-03-10 17:04:05 +06:00
97339c9b1d update wrapper version 2022-03-10 15:55:45 +06:00
eb35903de9 Update CHANGELOG.md 2022-03-10 15:20:42 +06:00
43e88e00da Update klock 2022-03-08 10:01:53 +06:00
1e51f9d77b Update CHANGELOG.md 2022-03-08 10:01:02 +06:00
dad738357c start 0.9.11 2022-03-08 10:00:19 +06:00
cb828ab3f2 Merge pull request #132 from InsanusMokrassar/0.9.10
0.9.10
2022-03-06 16:24:51 +06:00
aefaf4a8cc update klock (again) 2022-03-06 16:08:02 +06:00
b29f37a251 update klock 2022-03-04 20:20:17 +06:00
88854020ac extend functionality of websockets 2022-03-04 20:16:59 +06:00
14ebe01fc6 improvements in includeWebsocketHandling 2022-03-04 15:31:32 +06:00
771aed0f0f UnifiedRequester#createStandardWebsocketFlow(String,DeserializationStrategy) 2022-03-04 15:20:16 +06:00
16c57fcd6a start 0.9.10 2022-03-04 15:17:36 +06:00
75397a7ccb Merge pull request #131 from InsanusMokrassar/0.9.9
0.9.9
2022-02-24 14:44:13 +06:00
b05404f828 IntersectionObserver 2022-02-24 00:26:07 +06:00
edea942874 small refactor of operatons in applyDiff 2022-02-23 23:02:31 +06:00
8a8f568b9a add applyDiff in Diff Utils and update korlibs klock version 2022-02-23 22:55:56 +06:00
4dc8d30c52 start 0.9.9 2022-02-23 22:32:05 +06:00
cb4e08e823 Merge pull request #130 from InsanusMokrassar/0.9.8
0.9.8
2022-02-19 21:44:56 +06:00
c443bf4fa0 update dependencies 2022-02-19 16:15:36 +06:00
a6982de822 start 0.9.8 2022-02-19 16:06:45 +06:00
4f1a663e75 fixes in ExposedOneToManyKeyValueRepo and includeWebsocketHandling 2022-02-19 16:06:33 +06:00
dab262d626 start 0.9.7 2022-02-19 16:06:33 +06:00
87a3f61ca6 fix in ExposedOneToManyKeyValueRepo 2022-02-07 20:43:43 +06:00
506e937a68 start 0.9.6 2022-02-07 20:39:44 +06:00
5a037c76dd Merge pull request #126 from InsanusMokrassar/0.9.5
0.9.5
2022-02-01 23:30:26 +06:00
313f622f7e Update CHANGELOG.md 2022-02-01 14:27:07 +06:00
6cba1fe1a2 Update klock 2022-02-01 14:25:24 +06:00
fd2d0e80b7 start 0.9.5 2022-02-01 14:24:12 +06:00
96ab2e8aca Merge pull request #125 from InsanusMokrassar/0.9.4
0.9.4
2022-01-18 18:58:49 +06:00
0202988cae several fixes in optionallyReverse 2022-01-17 16:11:55 +06:00
d619d59947 Either updates 2022-01-17 15:56:48 +06:00
85b3e48d18 add optionallyReverse extensions 2022-01-17 15:38:23 +06:00
7a9b7d98a1 start 0.9.4 2022-01-14 13:22:04 +06:00
b212acfcaf Merge pull request #124 from InsanusMokrassar/0.9.3
0.9.3
2022-01-14 00:46:20 +06:00
3a45e5dc70 Update CHANGELOG.md 2022-01-14 00:37:03 +06:00
73190518d5 Update gradle.properties 2022-01-14 00:35:51 +06:00
03f78180dc Merge pull request #123 from InsanusMokrassar/0.9.2
0.9.2
2022-01-13 11:22:54 +06:00
1c0b8cf842 Update CHANGELOG.md 2022-01-13 10:20:35 +06:00
a1624ea2a9 Update gradle.properties 2022-01-13 10:19:08 +06:00
23a050cf1e Merge pull request #122 from InsanusMokrassar/0.9.1
0.9.1
2022-01-08 16:12:04 +06:00
916f2f96f4 typealiases for exposed one to many 2022-01-08 14:35:28 +06:00
00cc214754 repo exposed updates 2022-01-08 14:14:44 +06:00
b2e38f72b9 start 0.9.1 2022-01-08 14:12:57 +06:00
e7107d238d Update dokka_push.yml 2021-12-30 02:55:19 +06:00
ed9ebdbd1a Merge pull request #121 from InsanusMokrassar/0.9.0
0.9.0
2021-12-30 02:52:34 +06:00
e80676d3d2 Update packages_push.yml 2021-12-29 19:43:19 +06:00
02d02fa8f2 update android tools build 2021-12-29 19:24:25 +06:00
bd783fb74f update dokka 2021-12-29 17:44:45 +06:00
50386adf70 ignore kotlin-js-store folder 2021-12-28 21:19:47 +06:00
f4ee6c2890 update exposed and adapt to new version of kotlin serialization 2021-12-28 21:17:17 +06:00
d45aef9fe5 start 0.9.0 2021-12-28 09:58:59 +06:00
a56cd3dddd Merge pull request #120 from InsanusMokrassar/0.8.9
0.8.9
2021-12-27 17:02:16 +06:00
419e7070ee more fixes to god of fixes 2021-12-27 17:02:00 +06:00
612cf40b5f small hotfix 2021-12-27 16:00:07 +06:00
8b39882e83 fixes in DefaultUpdatableStatesMachine 2021-12-27 15:57:55 +06:00
e639ae172b Fixes in uniloadMultipart 2021-12-27 15:55:05 +06:00
d0446850ae start 0.8.9 2021-12-27 15:45:44 +06:00
c48465b90b Merge pull request #119 from InsanusMokrassar/0.8.8
0.8.8
2021-12-26 22:11:04 +06:00
f419fd03d2 small fix of performing state update for UpdatableStatesMachine 2021-12-26 22:00:26 +06:00
494812a660 one more fix 2021-12-26 21:25:12 +06:00
eb78f21eec fix FSMBuilder 2021-12-26 21:21:40 +06:00
4bda70268b small fix of style in DefaultUpdatableStatesMachine 2021-12-26 21:07:40 +06:00
f037ce4371 updates in FSM 2021-12-26 21:06:26 +06:00
3d2196e35d update dependencies (which possible) 2021-12-26 20:07:13 +06:00
a74f061b02 start 0.8.8 2021-12-26 20:02:18 +06:00
11ade14676 Merge pull request #118 from InsanusMokrassar/0.8.7
0.8.7
2021-12-15 01:22:42 +06:00
eb562d8784 add stream using for multipart 2021-12-14 22:03:29 +06:00
1ee5b4bfd4 hotfix for multipart 2021-12-14 21:13:11 +06:00
d97892080b add preview work with multipart 2021-12-14 18:01:41 +06:00
6f37125724 start 0.8.7 and make UnifiedRequester/UnifiedRouter fields are public 2021-12-14 13:25:58 +06:00
ed1baaade7 Merge pull request #117 from InsanusMokrassar/0.8.6
0.8.6
2021-12-08 15:57:43 +06:00
bb9669f8fd fill changelog 2021-12-08 11:45:01 +06:00
bdac715d48 remove crossinline where possible 2021-12-08 11:41:49 +06:00
acf4971298 downgrade ktor 2021-12-08 11:36:24 +06:00
249bc83a8c update ktor 2021-11-27 16:22:13 +06:00
0fbb92f03f start 0.8.6 2021-11-27 16:20:56 +06:00
ca27cb3f82 Merge pull request #116 from InsanusMokrassar/0.8.5
0.8.5
2021-11-27 01:00:18 +06:00
3a5771a0cc Update RepeatOnFailure.kt 2021-11-26 23:07:00 +06:00
527a2a91ac repeatOnFailure 2021-11-26 19:45:59 +06:00
6763e5c4c6 start 0.8.5 2021-11-26 19:32:52 +06:00
06918d8310 add mime types generator python script 2021-11-24 14:31:24 +06:00
89ccaa1b57 Merge pull request #115 from InsanusMokrassar/0.8.4
0.8.4
2021-11-19 14:02:38 +06:00
5d0bdb9bcf fix pagination 2021-11-19 13:58:59 +06:00
31fdcf74a5 ktor server createKtorServer extensions 2021-11-19 13:24:45 +06:00
afca09cc1d start 0.8.4 2021-11-19 13:18:37 +06:00
531d89d9db Merge pull request #114 from InsanusMokrassar/0.8.3
0.8.3
2021-11-19 09:49:35 +06:00
6bbbea0bc3 suppressions in Optional.kt 2021-11-18 23:03:24 +06:00
e337cd98c8 optional workaround 2021-11-17 21:31:35 +06:00
bcbab3b380 add Optional type 2021-11-17 17:38:41 +06:00
fb63de7568 intersect 2021-11-17 14:22:45 +06:00
aa45a4ab13 now Pagination is an ClosedRange 2021-11-17 14:07:11 +06:00
2af7e2f681 start 0.8.3 2021-11-17 14:01:44 +06:00
34fd9edce0 Merge pull request #113 from InsanusMokrassar/0.8.2
0.8.2
2021-11-12 13:23:39 +06:00
2a4cb8c5f9 improvements in FSM 2021-11-12 13:19:15 +06:00
50ea40bc3a add changelog for dependencies update 2021-11-12 13:14:57 +06:00
a77654052d Update gradle.properties 2021-11-11 21:20:17 +06:00
88aafce552 start 0.8.2 2021-11-11 20:31:04 +06:00
4e95d6bfff Merge pull request #112 from InsanusMokrassar/0.8.1
0.8.1
2021-11-09 14:04:11 +06:00
38d0e34fb5 updates in scripts and update core ktx 2021-11-09 14:03:51 +06:00
8fbc6b9041 update changelog 2021-11-09 13:43:46 +06:00
e8219d6cf4 start 0.8.1 2021-11-09 10:21:23 +06:00
6c20fc4ca6 Merge pull request #111 from InsanusMokrassar/0.8.0
0.8.0
2021-11-05 21:56:00 +06:00
85cd975492 preparations for release 2021-11-05 21:45:01 +06:00
1171a717fe optimize imports 2021-11-05 15:49:39 +06:00
bbe5320312 rework of FSM + 0.8.0 2021-11-05 15:43:05 +06:00
00acb9fddd add suppresses to the generated classes 2021-11-03 17:22:32 +06:00
de3d14dc41 solution of #109 2021-11-03 15:36:25 +06:00
67ff9cc9b3 update dependencies 2021-11-03 15:18:41 +06:00
af132103a0 start 0.7.5 and add either serializer 2021-11-02 12:43:59 +06:00
3b1124a804 fixes 2021-10-30 12:17:18 +06:00
f226c2dfd6 Merge pull request #106 from InsanusMokrassar/0.7.4
0.7.4
2021-10-29 13:57:58 +06:00
69d6e63846 getAllBy* 2021-10-29 13:51:59 +06:00
02c3d397ad solution of #104 2021-10-29 13:40:53 +06:00
67a1050646 solution for #105 2021-10-28 18:46:53 +06:00
8cd0775a6c update kdocs of Either 2021-10-28 18:42:05 +06:00
162294d6c6 either 2021-10-28 18:02:02 +06:00
c4dd19dd00 start 0.7.4 2021-10-28 17:50:28 +06:00
d2314422f1 Merge pull request #103 from InsanusMokrassar/0.7.3
0.7.3
2021-10-23 14:26:42 +06:00
6fedd6f859 update dependencies 2021-10-23 14:24:31 +06:00
e52b59665f start 0.7.3 2021-10-23 14:19:30 +06:00
cda9d09689 fixes for kdocs 2021-10-18 22:57:25 +06:00
c9237b3f00 Merge pull request #102 from InsanusMokrassar/0.7.2
0.7.2
2021-10-16 09:51:24 +06:00
107 changed files with 2847 additions and 505 deletions

View File

@@ -10,10 +10,10 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Build
run: ./gradlew dokkaHtml
- name: Publish KDocs

View File

@@ -8,10 +8,10 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
@@ -22,7 +22,7 @@ jobs:
run: ./gradlew build
- name: Publish
continue-on-error: true
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -11,5 +11,6 @@ out/
secret.gradle
local.properties
kotlin-js-store
publishing.sh

View File

@@ -1,5 +1,312 @@
# Changelog
## 0.9.22
* `Ktor`:
* `Server`:
* Now `createKtorServer` fun is fully customizable
## 0.9.21
* `Repos`:
* `Exposed`:
* fixes in `AbstractExposedWriteCRUDRepo`
## 0.9.20
* `Repos`:
* `Common`:
* Fixes in `OneToManyAndroidRepo`
* New `CursorIterator`
## 0.9.19
* `Versions`:
* `Coroutines`: `1.6.0` -> `1.6.1`
* `Repos`:
* `Exposed`:
* Fixes in `ExposedStandardVersionsRepoProxy`
## 0.9.18
* `Common`
* New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
## 0.9.17
* `Common`:
* New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
* `Coroutines`:
* New extension `Element.visibilityFlow()`
* `FSM`:
* Now it is possible to resolve conflicts on `startChain`
## 0.9.16
* `Versions`:
* `Klock`: `2.6.3` -> `2.7.0`
* `Common`:
* New extension `Node#onRemoved`
* `Compose`:
* New extension `Composition#linkWithRoot` for removing of composition with root element
* `Coroutines`:
* `Compose`:
* New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
## 0.9.15
* `FSM`:
* Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
* `DefaultStatesManager` now is extendable
* `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
## 0.9.14
* `Versions`:
* `Klock`: `2.6.2` -> `2.6.3`
* `Ktor`: `1.6.7` -> `1.6.8`
* `Ktor`:
* Add temporal files uploading functionality (for clients to upload and for server to receive)
## 0.9.13
* `Versions`:
* `Compose`: `1.1.0` -> `1.1.1`
## 0.9.12
* `Common`:
* `JS`:
* New function `openLink`
* New function `selectFile`
* New function `triggerDownloadFile`
* `Compose`:
* Created :)
* `Common`:
* `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
* `JS`:
* `openLink` on top of `openLink` with `String` target from common
* `Coroutines`:
* `Compose`:
* `Common`:
* New extension `Flow.toMutableState`
* New extension `StateFlow.toMutableState`
* `JS`:
* New function `selectFileOrThrow` on top of `selectFile` from `common`
* New function `selectFileOrNull` on top of `selectFile` from `common`
## 0.9.11
* `Versions`:
* `Klock`: `2.6.1` -> `2.6.2`
* `Coroutines`:
* `Compose`:
* Created :)
* New extensions and function:
* `Composition#linkWithJob`
* `Composition#linkWithContext`
* `renderComposableAndLinkToContext`
## 0.9.10
* `Versions`:
* `Klock`: `2.5.2` -> `2.6.1`
* Ktor:
* Client:
* New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
* Server:
* Now it is possible to filter data in `Route#includeWebsocketHandling`
* Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
* Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
## 0.9.9
* `Versions`:
* `Klock`: `2.5.1` -> `2.5.2`
* `Common`:
* Add new diff tool - `applyDiff`
* Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
## 0.9.8
* `Versions`:
* `Exposed`: `0.37.2` -> `0.37.3`
* `Klock`: `2.4.13` -> `2.5.1`
* `AppCompat`: `1.4.0` -> `1.4.1`
## 0.9.7
* `Repos`:
* `Exposed`:
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore`
* `Ktor`:
* `Server`:
* `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not
## 0.9.6
* `Repos`:
* `Exposed`:
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere`
## 0.9.5
* `Versions`:
* `Klock`: `2.4.12` -> `2.4.13`
## 0.9.4
* `Pagination`:
* `Common`:
* Add several `optionallyReverse` functions
* `Common`:
* Changes in `Either`:
* Now `Either` uses `optionalT1` and `optionalT2` as main properties
* `Either#t1` and `Either#t2` are deprecated
* New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
## 0.9.3
* `Versions`:
* `UUID`: `0.3.1` -> `0.4.0`
## 0.9.2
* `Versions`:
* `Klock`: `2.4.10` -> `2.4.12`
## 0.9.1
* `Repos`:
* `Exposed`:
* Default realizations of standard interfaces for exposed DB are using public fields for now:
* `ExposedReadKeyValueRepo`
* `ExposedReadOneToManyKeyValueRepo`
* `ExposedStandardVersionsRepoProxy`
* New typealiases for one to many exposed realizations:
* `ExposedReadKeyValuesRepo`
* `ExposedKeyValuesRepo`
## 0.9.0
* `Versions`:
* `Kotlin`: `1.5.31` -> `1.6.10`
* `Coroutines`: `1.5.2` -> `1.6.0`
* `Serialization`: `1.3.1` -> `1.3.2`
* `Exposed`: `0.36.2` -> `0.37.2`
* `Ktor`: `1.6.5` -> `1.6.7`
* `Klock`: `2.4.8` -> `2.4.10`
## 0.8.9
* `Ktor`:
* `Server`:
* Fixes in `uniloadMultipart`
* `Client`:
* Fixes in `unimultipart`
* `FSM`:
* Fixes in `DefaultUpdatableStatesMachine`
## 0.8.8
* `Versions`:
* `AppCompat`: `1.3.1` -> `1.4.0`
* Android Compile SDK: `31.0.0` -> `32.0.0`
* `FSM`:
* `DefaultStatesMachine` now is extendable
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
## 0.8.7
* `Ktor`:
* `Client`:
* `UnifiedRequester` now have no private fields
* Add preview work with multipart
* `Server`
* `UnifiedRouter` now have no private fields
* Add preview work with multipart
## 0.8.6
* `Common`:
* `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
* All `joinTo` now accept not `crossinline` callbacks
## 0.8.5
* `Common`:
* `repeatOnFailure`
## 0.8.4
* `Ktor`:
* `Server`:
* Several new `createKtorServer`
## 0.8.3
* `Common`:
* Ranges intersection functionality
* New type `Optional`
* `Pagination`:
* `Pagination` now extends `ClosedRange<Int>`
* `Pagination` intersection functionality
## 0.8.2
* `Versions`:
* `Klock`: `2.4.7` -> `2.4.8`
* `Serialization`: `1.3.0` -> `1.3.1`
* `FSM`:
* Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
* Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
## 0.8.1
* `Versions`:
* `Exposed`: `0.36.1` -> `0.36.2`
* `Core KTX`: `1.6.0` -> `1.7.0`
## 0.8.0
* `Versions`:
* `Klock`: `2.4.6` -> `2.4.7`
* `Ktor`: `1.6.4` -> `1.6.5`
* `Exposed`: `0.35.3` -> `0.36.1`
* `Common`:
* Type `Either` got its own serializer
* `FSM`:
* `Common`:
* Full rework of FSM:
* Now it is more flexible for checking of handler opportunity to handle state
* Now machine and states managers are type-oriented
* `StateHandlerHolder` has been renamed to `CheckableHandlerHolder`
* Add opportunity for comfortable adding default state handler
## 0.7.4
* `Common`:
* New type `Either`
* `Serialization`:
* `TypedSerializer`
* New factory fun which accept vararg pairs of type and its serializer
* `Repos`:
* `Common` (`Android`):
* `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that
android crud repo _WILL NOT_ send previous events to the
* `Exposed`:
* New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows`
* KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in
inheritors
* `Pagination`:
* `Common`:
* New types `getAllBy*` for current, next and custom paging
## 0.7.3
* `Versions`:
* `Exposed`: `0.35.2` -> `0.35.3`
## 0.7.2
* `Versions`:

View File

@@ -10,7 +10,7 @@ kotlin {
sourceSets {
androidMain {
dependencies {
api "androidx.appcompat:appcompat-resources:$appcompat_version"
api libs.android.appCompat.resources
}
}
}

View File

@@ -10,13 +10,13 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api libs.kt.coroutines
api project(":micro_utils.common")
}
}
androidMain {
dependencies {
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
api libs.android.recyclerView
}
}
}

View File

@@ -7,12 +7,12 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization
classpath libs.buildscript.jb.dokka
classpath libs.buildscript.gh.release
classpath libs.buildscript.android.gradle
classpath libs.buildscript.android.dexcount
}
}

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
}
}
}
}

View File

@@ -0,0 +1,16 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.DisposableEffectResult
class DefaultDisposableEffectResult(
private val onDispose: () -> Unit
) : DisposableEffectResult {
override fun dispose() {
onDispose()
}
companion object {
val DoNothing = DefaultDisposableEffectResult {}
}
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.Composition
import dev.inmo.micro_utils.common.onRemoved
import org.w3c.dom.Element
fun Composition.linkWithElement(element: Element) {
element.onRemoved { dispose() }
}

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.common.compose
import org.jetbrains.compose.web.attributes.ATarget
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
link,
mode.targetStr,
features
)

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
fun <TElement : Element> renderComposableAndLinkToRoot(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithElement(root)
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.common.compose"/>

View File

@@ -2,8 +2,6 @@
package dev.inmo.micro_utils.common
import kotlin.jvm.JvmInline
private inline fun <T> getObject(
additional: MutableList<T>,
iterator: Iterator<T>
@@ -155,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
inline fun <T> Iterable<T>.calculateStrictDiff(
other: Iterable<T>
) = calculateDiff(other, strictComparison = true)
/**
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
* mutable list
*/
fun <T> MutableList<T>.applyDiff(
source: Iterable<T>,
strictComparison: Boolean = false
) = calculateDiff(source, strictComparison).let {
for (i in it.removed.indices.sortedDescending()) {
removeAt(it.removed[i].index)
}
it.added.forEach { (i, t) ->
add(i, t)
}
it.replaced.forEach { (_, new) ->
set(new.index, new.value)
}
}

View File

@@ -0,0 +1,166 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
/**
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
*
* @see EitherFirst
* @see EitherSecond
* @see Either.Companion.first
* @see Either.Companion.second
* @see Either.onFirst
* @see Either.onSecond
* @see Either.mapOnFirst
* @see Either.mapOnSecond
*/
@Serializable(EitherSerializer::class)
sealed interface Either<T1, T2> {
val optionalT1: Optional<T1>
val optionalT2: Optional<T2>
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
val t1: T1?
get() = optionalT1.dataOrNull()
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
val t2: T2?
get() = optionalT2.dataOrNull()
companion object {
fun <T1, T2> serializer(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
}
}
class EitherSerializer<T1, T2>(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
) : KSerializer<Either<T1, T2>> {
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
override val descriptor: SerialDescriptor = buildSerialDescriptor(
"TypedSerializer",
SerialKind.CONTEXTUAL
) {
element("type", String.serializer().descriptor)
element("value", ContextualSerializer(Either::class).descriptor)
}
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
override fun deserialize(decoder: Decoder): Either<T1, T2> {
return decoder.decodeStructure(descriptor) {
var type: String? = null
lateinit var result: Either<T1, T2>
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> type = decodeStringElement(descriptor, 0)
1 -> {
result = when (type) {
"t1" -> decodeSerializableElement(
descriptor,
1,
t1EitherSerializer
)
"t2" -> decodeSerializableElement(
descriptor,
1,
t2EitherSerializer
)
else -> error("Unknown type of either: $type")
}
}
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
result
}
}
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
encoder.encodeStructure(descriptor) {
when (value) {
is EitherFirst -> {
encodeStringElement(descriptor, 0, "t1")
encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
}
is EitherSecond -> {
encodeStringElement(descriptor, 0, "t2")
encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
}
}
}
}
}
/**
* This type [Either] will always have not nullable [optionalT1]
*/
@Serializable
data class EitherFirst<T1, T2>(
override val t1: T1
) : Either<T1, T2> {
override val optionalT1: Optional<T1> = t1.optional
override val optionalT2: Optional<T2> = Optional.absent()
}
/**
* This type [Either] will always have not nullable [optionalT2]
*/
@Serializable
data class EitherSecond<T1, T2>(
override val t2: T2
) : Either<T1, T2> {
override val optionalT1: Optional<T1> = Optional.absent()
override val optionalT2: Optional<T2> = t2.optional
}
/**
* @return New instance of [EitherFirst]
*/
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
/**
* @return New instance of [EitherSecond]
*/
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
/**
* Will call [block] in case when [this] is [EitherFirst]
*/
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
optionalT1.onPresented(block)
return this
}
/**
* Will call [block] in case when [this] is [EitherSecond]
*/
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
optionalT2.onPresented(block)
return this
}
/**
* @return Result of [block] if [this] is [EitherFirst]
*/
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
return optionalT1.mapOnPresented(block)
}
/**
* @return Result of [block] if [this] is [EitherSecond]
*/
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
return optionalT2.mapOnPresented(block)
}
inline fun <reified T1, reified T2> Any.either() = when (this) {
is T1 -> Either.first<T1, T2>(this)
is T2 -> Either.second<T1, T2>(this)
else -> error("Incorrect type of either argument $this")
}

View File

@@ -1,10 +1,10 @@
package dev.inmo.micro_utils.common
inline fun <I, R> Iterable<I>.joinTo(
crossinline separatorFun: (I) -> R?,
separatorFun: (I) -> R?,
prefix: R? = null,
postfix: R? = null,
crossinline transform: (I) -> R?
transform: (I) -> R?
): List<R> {
val result = mutableListOf<R>()
val iterator = iterator()
@@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo(
separator: R? = null,
prefix: R? = null,
postfix: R? = null,
crossinline transform: (I) -> R?
transform: (I) -> R?
): List<R> = joinTo({ separator }, prefix, postfix, transform)
inline fun <I> Iterable<I>.joinTo(
crossinline separatorFun: (I) -> I?,
separatorFun: (I) -> I?,
prefix: I? = null,
postfix: I? = null
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
@@ -45,15 +45,15 @@ inline fun <I> Iterable<I>.joinTo(
): List<I> = joinTo<I>({ separator }, prefix, postfix)
inline fun <I, reified R> Array<I>.joinTo(
crossinline separatorFun: (I) -> R?,
separatorFun: (I) -> R?,
prefix: R? = null,
postfix: R? = null,
crossinline transform: (I) -> R?
transform: (I) -> R?
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
inline fun <I, reified R> Array<I>.joinTo(
separator: R? = null,
prefix: R? = null,
postfix: R? = null,
crossinline transform: (I) -> R?
transform: (I) -> R?
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()

View File

@@ -23,11 +23,12 @@ value class FileName(val string: String) {
}
@PreviewFeature
expect class MPPFile
expect val MPPFile.filename: FileName
expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
fun MPPFile.bytesSync() = bytesAllocatorSync()
suspend fun MPPFile.bytes() = bytesAllocator()

View File

@@ -0,0 +1,92 @@
@file:Suppress("unused")
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
/**
* This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
* type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
* presented by some third type.
*
* Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
* will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
* cause this null will say nothing about availability of the row (of course, it is exaggerated example)
*
* @see Optional.presented
* @see Optional.absent
* @see Optional.optional
* @see Optional.onPresented
* @see Optional.onAbsent
*/
@Serializable
data class Optional<T> internal constructor(
@Warning("It is unsafe to use this data directly")
val data: T?,
@Warning("It is unsafe to use this data directly")
val dataPresented: Boolean
) {
companion object {
/**
* Will create [Optional] with presented data
*/
fun <T> presented(data: T) = Optional(data, true)
/**
* Will create [Optional] without data
*/
fun <T> absent() = Optional<T>(null, false)
}
}
inline val <T> T.optional
get() = Optional.presented(this)
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
}
/**
* Will call [block] when data absent ([Optional.dataPresented] == false)
*/
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
if (!dataPresented) { block() }
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
}
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
*/
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
*/
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()

View File

@@ -0,0 +1,19 @@
package dev.inmo.micro_utils.common
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
start > other.endInclusive || other.start > endInclusive -> null
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
}
fun IntRange.intersect(
other: IntRange
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
it.first .. it.second
}
fun LongRange.intersect(
other: LongRange
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
it.first .. it.second
}

View File

@@ -0,0 +1,21 @@
package dev.inmo.micro_utils.common
/**
* Executes the given [action] until getting of successful result specified number of [times].
*
* A zero-based index of current iteration is passed as a parameter to [action].
*/
inline fun <R> repeatOnFailure(
times: Int,
onEachFailure: (Throwable) -> Unit = {},
action: (Int) -> R
): Optional<R> {
repeat(times) {
runCatching {
action(it)
}.onFailure(onEachFailure).onSuccess {
return Optional.presented(it)
}
}
return Optional.absent()
}

View File

@@ -54,7 +54,7 @@ class DiffUtilsTests {
val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex()
for (step in 0 until oldList.size) {
for (step in oldList.indices) {
for ((i, v) in withIndex) {
val mutable = oldList.toMutableList()
val changes = (
@@ -73,4 +73,78 @@ class DiffUtilsTests {
}
}
}
@Test
fun testThatSimpleRemoveApplyWorks() {
val oldList = (0 until 10).toList()
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, _) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
val removedSublist = oldList.subList(i, i + count)
val mutableOldList = oldList.toMutableList()
val targetList = oldList - removedSublist
mutableOldList.applyDiff(targetList)
assertEquals(
targetList,
mutableOldList
)
}
}
}
@Test
fun testThatSimpleAddApplyWorks() {
val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, v) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
val mutable = oldList.toMutableList()
mutable.addAll(i, addedSublist)
val mutableOldList = oldList.toMutableList()
mutableOldList.applyDiff(mutable)
assertEquals(
mutable,
mutableOldList
)
}
}
}
@Test
fun testThatSimpleChangesApplyWorks() {
val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex()
for (step in oldList.indices) {
for ((i, v) in withIndex) {
val mutable = oldList.toMutableList()
val changes = (
if (step == 0) i until oldList.size else (i until oldList.size step step)
).map { index ->
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
mutable[index] = it.value
}
}
val mutableOldList = oldList.toMutableList()
mutableOldList.applyDiff(mutable)
assertEquals(
mutable,
mutableOldList
)
}
}
}
}

View File

@@ -0,0 +1,61 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
fun Node.onRemoved(block: () -> Unit): MutationObserver {
lateinit var observer: MutationObserver
observer = MutationObserver { _, _ ->
fun checkIfRemoved(node: Node): Boolean {
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
}
if (checkIfRemoved(this)) {
observer.disconnect()
block()
}
}
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
return observer
}
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
var previousIntersectionRatio = -1f
val observer = IntersectionObserver { entries, observer ->
entries.forEach {
if (previousIntersectionRatio != it.intersectionRatio) {
previousIntersectionRatio = it.intersectionRatio.toFloat()
it.block(previousIntersectionRatio, observer)
}
}
}
observer.observe(this)
return observer
}
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
var previous = -1f
onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio > 0 && previous == 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
var previous = -1f
return onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio == 0f && previous != 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}

View File

@@ -0,0 +1,124 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.DOMRectReadOnly
import org.w3c.dom.Element
external interface IntersectionObserverOptions {
/**
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
* visible.
*/
var root: Element?
/**
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
*/
var rootMargin: String?
/**
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
*/
var threshold: Array<Number>?
}
fun IntersectionObserverOptions(
block: IntersectionObserverOptions.() -> Unit = {}
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
external interface IntersectionObserverEntry {
/**
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
* the documentation for Element.getBoundingClientRect().
*/
val boundingClientRect: DOMRectReadOnly
/**
* Returns the ratio of the intersectionRect to the boundingClientRect.
*/
val intersectionRatio: Number
/**
* Returns a DOMRectReadOnly representing the target's visible area.
*/
val intersectionRect: DOMRectReadOnly
/**
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
* then you know the transition is from intersecting to not-intersecting.
*/
val isIntersecting: Boolean
/**
* Returns a DOMRectReadOnly for the intersection observer's root.
*/
val rootBounds: DOMRectReadOnly
/**
* The Element whose intersection with the root changed.
*/
val target: Element
/**
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
* IntersectionObserver's time origin.
*/
val time: Double
}
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
/**
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
*/
external class IntersectionObserver(callback: IntersectionObserverCallback) {
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
/**
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
* was passed to the constructor or its value is null, the top-level document's viewport is used.
*/
val root: Element
/**
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
*/
val rootMargin: String
/**
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
* crossed for that target. If no value was passed to the constructor, 0 is used.
*/
val thresholds: Array<Number>
/**
* Stops the IntersectionObserver object from observing any target.
*/
fun disconnect()
/**
* Tells the IntersectionObserver a target element to observe.
*/
fun observe(targetElement: Element)
/**
* Returns an array of IntersectionObserverEntry objects for all observed targets.
*/
fun takeRecords(): Array<IntersectionObserverEntry>
/**
* Tells the IntersectionObserver to stop observing a particular target element.
*/
fun unobserve(targetElement: Element)
}

View File

@@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent
import org.w3c.files.File
import org.w3c.files.FileReader
import org.w3c.files.*
import kotlin.js.Promise
/**
* @suppress
*/
actual typealias MPPFile = File
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
@@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
reader.readAsArrayBuffer(this)
}
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = size.toLong()
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes

View File

@@ -0,0 +1,38 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
lateinit var observer: MutationObserver
val listener = EventListener {
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
val childrenGettingElement = elementsToCheck.removeFirst()
for (i in 0 until childrenGettingElement.childElementCount) {
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
}
}
if (elementsToCheck.isEmpty()) {
callback(it)
}
}
if (options == null) {
document.addEventListener(type, listener)
} else {
document.addEventListener(type, listener, options)
}
observer = onRemoved {
if (options == null) {
document.removeEventListener(type, listener)
} else {
document.removeEventListener(type, listener, options)
}
observer.disconnect()
}
return listener
}
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun openLink(link: String, target: String = "_blank", features: String = "") {
window.open(link, target, features) ?.focus()
}

View File

@@ -0,0 +1,30 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import kotlinx.dom.createElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.files.get
fun selectFile(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {},
onFile: (MPPFile) -> Unit
) {
(document.createElement("input") {
(this as HTMLInputElement).apply {
type = "file"
onchange = {
runCatching {
files ?.get(0) ?: error("File must not be null")
}.onSuccess {
onFile(it)
}.onFailure {
onFailure(it)
}
}
inputSetup(this)
}
} as HTMLElement).click()
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
fun triggerDownloadFile(filename: String, fileLink: String) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
hiddenElement.href = fileLink
hiddenElement.target = "_blank"
hiddenElement.download = filename
hiddenElement.click()
}

View File

@@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
import java.io.File
/**
* @suppress
*/
actual typealias MPPFile = File
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = length()
/**
* @suppress
*/
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = {
doInIO {

View File

@@ -10,12 +10,17 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api libs.kt.coroutines
}
}
jsMain {
dependencies {
api project(":micro_utils.common")
}
}
androidMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
api libs.kt.coroutines.android
}
}
}

View File

@@ -0,0 +1,20 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api libs.kt.coroutines
api project(":micro_utils.coroutines")
api project(":micro_utils.common.compose")
}
}
}
}

View File

@@ -0,0 +1,22 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
fun <T> Flow<T>.toMutableState(
initial: T,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
inline fun <T> StateFlow<T>.toMutableState(
scope: CoroutineScope
): MutableState<T> = toMutableState(value, scope)

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlin.coroutines.CoroutineContext
fun Composition.linkWithJob(job: Job) {
job.invokeOnCompletion {
this@linkWithJob.dispose()
}
}
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)

View File

@@ -0,0 +1,26 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.common.compose.linkWithElement
import kotlinx.coroutines.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
suspend fun <TElement : Element> renderComposableAndLinkToContext(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(
currentCoroutineContext()
)
}
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(currentCoroutineContext())
linkWithElement(root)
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>

View File

@@ -0,0 +1,28 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.common.onRemoved
import dev.inmo.micro_utils.common.onVisibilityChanged
import kotlinx.coroutines.flow.*
import org.w3c.dom.Element
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
var previousData: Boolean? = null
val observer = onVisibilityChanged { intersectionRatio, _ ->
val currentData = intersectionRatio > 0
if (currentData != previousData) {
trySend(currentData)
}
previousData = currentData
}
val removeObserver = onRemoved {
observer.disconnect()
close()
}
invokeOnClose {
observer.disconnect()
removeObserver.disconnect()
}
}

View File

@@ -0,0 +1,42 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.selectFile
import kotlinx.coroutines.CompletableDeferred
import org.w3c.dom.HTMLInputElement
suspend fun selectFileOrThrow(
inputSetup: (HTMLInputElement) -> Unit = {}
): MPPFile {
val result = CompletableDeferred<MPPFile>()
selectFile(
inputSetup,
{
result.completeExceptionally(it)
}
) {
result.complete(it)
}
return result.await()
}
suspend fun selectFileOrNull(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {}
): MPPFile? {
val result = CompletableDeferred<MPPFile?>()
selectFile(
inputSetup,
{
result.complete(null)
onFailure(it)
}
) {
result.complete(it)
}
return result.await()
}

View File

@@ -1,3 +1,6 @@
package dev.inmo.micro_utils.crypto
/**
* @suppress
*/
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())

View File

@@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto
import java.math.BigInteger
import java.security.MessageDigest
/**
* @suppress
*/
actual fun SourceBytes.md5(): MD5 = BigInteger(
1,
MessageDigest.getInstance("MD5").digest(this)

View File

@@ -26,12 +26,12 @@ ext {
}
android {
compileSdkVersion "$android_compileSdkVersion".toInteger()
buildToolsVersion "$android_buildToolsVersion"
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
buildToolsVersion libs.versions.android.props.buildTools.get()
defaultConfig {
minSdkVersion "$android_minSdkVersion".toInteger()
targetSdkVersion "$android_compileSdkVersion".toInteger()
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
}

View File

@@ -13,10 +13,10 @@ repositories {
kotlin {
jvm()
js(IR) {
browser()
nodejs()
}
// js(IR) {
// browser()
// nodejs()
// }
android {}
sourceSets {
@@ -29,7 +29,7 @@ kotlin {
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) {
@@ -38,22 +38,22 @@ kotlin {
}
}
}
jsMain {
dependencies {
implementation kotlin('stdlib')
// jsMain {
// dependencies {
// implementation kotlin('stdlib')
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
) {
api it
}
}
}
}
// project.parent.subprojects.forEach {
// if (
// it != project
// && it.hasProperty("kotlin")
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
// ) {
// api it
// }
// }
// }
// }
jvmMain {
dependencies {
implementation kotlin('stdlib')
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
sourceRoots.setFrom(findSourcesWithName("commonMain"))
}
named("jsMain") {
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
}
// named("jsMain") {
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
// }
named("jvmMain") {
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))

View File

@@ -21,6 +21,7 @@ allprojects {
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"

View File

@@ -10,6 +10,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
api project(":micro_utils.coroutines")
}
}

View File

@@ -0,0 +1,81 @@
package dev.inmo.micro_utils.fsm.common
import kotlin.reflect.KClass
/**
* Define checkable holder which can be used to precheck that this handler may handle incoming [State]
*/
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
suspend fun checkHandleable(state: O): Boolean
}
/**
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
* casting in [handleState]
*/
class CustomizableHandlerHolder<I : O, O : State>(
private val delegateTo: StatesHandler<I, O>,
private val filter: suspend (state: O) -> Boolean
) : CheckableHandlerHolder<I, O> {
/**
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
*/
override suspend fun checkHandleable(state: O) = filter(state)
/**
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
* to be sure that this [StatesHandlerHolder] will be able to handle [state]
*/
override suspend fun StatesMachine<in O>.handleState(state: I): O? {
return delegateTo.run { handleState(state) }
}
}
fun <I : O, O : State> CheckableHandlerHolder(
inputKlass: KClass<I>,
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CustomizableHandlerHolder(
StatesHandler<O, O> {
delegateTo.run { handleState(it as I) }
},
if (strict) {
{ it::class == inputKlass }
} else {
{ inputKlass.isInstance(it) }
}
)
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
fun <I : O, O : State> StateHandlerHolder(
inputKlass: KClass<I>,
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CheckableHandlerHolder(inputKlass, strict, delegateTo)
inline fun <reified I : O, O : State> CheckableHandlerHolder(
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CheckableHandlerHolder(I::class, strict, delegateTo)
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
inline fun <reified I : O, O : State> StateHandlerHolder(
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CheckableHandlerHolder(strict, delegateTo)
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
strict: Boolean = true
) = CheckableHandlerHolder<I, O>(
I::class,
strict,
this
)
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
noinline filter: suspend (state: State) -> Boolean
) = CustomizableHandlerHolder<O, O>(
{ this@holder.run { handleState(it as I) } },
filter
)

View File

@@ -1,6 +0,0 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
@Deprecated("Replaced", ReplaceWith("InMemoryStatesManager", "dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager"))
typealias InMemoryStatesManager = InMemoryStatesManager

View File

@@ -1,27 +0,0 @@
package dev.inmo.micro_utils.fsm.common
import kotlin.reflect.KClass
/**
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
* casting in [handleState]
*/
class StateHandlerHolder<I : State>(
private val inputKlass: KClass<I>,
private val strict: Boolean = false,
private val delegateTo: StatesHandler<I>
) : StatesHandler<State> {
/**
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
*/
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
/**
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
* to be sure that this [StateHandlerHolder] will be able to handle [state]
*/
override suspend fun StatesMachine.handleState(state: State): State? {
return delegateTo.run { handleState(state as I) }
}
}

View File

@@ -3,10 +3,10 @@ package dev.inmo.micro_utils.fsm.common
/**
* Default realization of states handler
*/
fun interface StatesHandler<I : State> {
fun interface StatesHandler<I : State, O: State> {
/**
* Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
* [State] it is assumed that chain is not completed.
*/
suspend fun StatesMachine.handleState(state: I): State?
suspend fun StatesMachine<in O>.handleState(state: I): O?
}

View File

@@ -1,24 +1,27 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.common.onPresented
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow
private suspend fun <I : State> StatesMachine.launchStateHandling(
state: State,
handlers: List<StateHandlerHolder<out I>>
): State? {
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
}
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
* [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
* handling until [start] method will be called
*/
interface StatesMachine : StatesHandler<State> {
interface StatesMachine<T : State> : StatesHandler<T, T> {
suspend fun launchStateHandling(
state: T,
handlers: List<CheckableHandlerHolder<in T, T>>
): T? {
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
}
/**
* Starts handling of [State]s
*/
@@ -27,31 +30,67 @@ interface StatesMachine : StatesHandler<State> {
/**
* Start chain of [State]s witn [state]
*/
suspend fun startChain(state: State)
suspend fun startChain(state: T)
companion object {
/**
* Creates [DefaultStatesMachine]
*/
operator fun invoke(
statesManager: StatesManager,
handlers: List<StateHandlerHolder<*>>
operator fun <T: State> invoke(
statesManager: StatesManager<T>,
handlers: List<CheckableHandlerHolder<in T, T>>
) = DefaultStatesMachine(statesManager, handlers)
}
}
/**
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
* resolving, and uses [launchStateHandling] for [State] handling
* resolving, and uses [launchStateHandling] for [State] handling.
*
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
*/
class DefaultStatesMachine (
private val statesManager: StatesManager,
private val handlers: List<StateHandlerHolder<*>>
) : StatesMachine {
open class DefaultStatesMachine <T: State>(
protected val statesManager: StatesManager<T>,
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
) : StatesMachine<T> {
/**
* Will call [launchStateHandling] for state handling
*/
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
/**
* This
*/
protected val statesJobs = mutableMapOf<T, Job>()
protected val statesJobsMutex = Mutex()
protected open suspend fun performUpdate(state: T) {
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
statesJobs[actualState] ?.cancel()
statesJobs[actualState] = scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
if (statesJobs[actualState] == job) {
statesJobs.remove(actualState)
}
}
}
}
}
}
}
/**
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
@@ -60,30 +99,32 @@ class DefaultStatesMachine (
* [StatesManager.endChain].
*/
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
val statePerformer: suspend (State) -> Unit = { state: State ->
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it) }
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
}
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it.second) }
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
}
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
launch {
statesJobsMutex.withLock {
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
if (stateInMap === removedState) {
statesJobs[stateInMap] ?.cancel()
}
}
}
}
statesManager.getActiveStates().forEach {
launch { statePerformer(it) }
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
}
}
/**
* Just calls [StatesManager.startChain] of [statesManager]
*/
override suspend fun startChain(state: State) {
override suspend fun startChain(state: T) {
statesManager.startChain(state)
}
}

View File

@@ -1,30 +1,30 @@
package dev.inmo.micro_utils.fsm.common
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
interface StatesManager {
val onChainStateUpdated: Flow<Pair<State, State>>
val onStartChain: Flow<State>
val onEndChain: Flow<State>
interface StatesManager<T : State> {
val onChainStateUpdated: Flow<Pair<T, T>>
val onStartChain: Flow<T>
val onEndChain: Flow<T>
/**
* Must set current set using [State.context]
*/
suspend fun update(old: State, new: State)
suspend fun update(old: T, new: T)
/**
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
* busy by the other [State]
*/
suspend fun startChain(state: State)
suspend fun startChain(state: T)
/**
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
* ignored
*/
suspend fun endChain(state: State)
suspend fun endChain(state: T)
suspend fun getActiveStates(): List<State>
suspend fun getActiveStates(): List<T>
}

View File

@@ -0,0 +1,72 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.common.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
/**
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
* must be able to perform update of chain in internal [StatesManager]
*/
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
/**
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
* [StatesManager.update] function
*/
suspend fun updateChain(currentState: T, newState: T)
}
open class DefaultUpdatableStatesMachine<T : State>(
statesManager: StatesManager<T>,
handlers: List<CheckableHandlerHolder<in T, T>>,
) : DefaultStatesMachine<T>(
statesManager,
handlers
), UpdatableStatesMachine<T> {
protected val jobsStates = mutableMapOf<Job, T>()
/**
* Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if
* [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In
* other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of
* states is solved in [shouldReplaceJob] and can be rewritten in subclasses
*/
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
if (compare(previousState, actualState)) {
statesJobs[actualState] ?.cancel()
}
val job = previousState.mapOnPresented {
statesJobs.remove(it)
} ?.takeIf { it.isActive } ?: scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
statesJobs.remove(
jobsStates[job] ?: return@withLock
)
}
}
}
}
jobsStates.remove(job)
statesJobs[actualState] = job
jobsStates[job] = actualState
}
}
/**
* Compare if [previous] potentially lead to the same behaviour with [new]
*/
protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
@Deprecated("Overwrite shouldReplaceJob instead")
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new)
override suspend fun updateChain(currentState: T, newState: T) {
statesManager.update(currentState, newState)
}
}

View File

@@ -1,37 +1,61 @@
package dev.inmo.micro_utils.fsm.common.dsl
import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.managers.*
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo
import kotlin.reflect.KClass
class FSMBuilder(
var statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo())
class FSMBuilder<T : State>(
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
StatesMachine(
statesManager,
states
)
},
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
) {
private var states = mutableListOf<StateHandlerHolder<*>>()
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
states.add(StateHandlerHolder(kClass, false, handler))
fun add(handler: CheckableHandlerHolder<T, T>) {
states.add(handler)
}
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
states.add(StateHandlerHolder(kClass, true, handler))
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
add(CheckableHandlerHolder(kClass, false, handler))
}
fun build() = StatesMachine(
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
add(handler.holder(filter))
}
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
states.add(CheckableHandlerHolder(kClass, true, handler))
}
inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) {
add(I::class, handler)
}
inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) {
addStrict(I::class, handler)
}
inline fun <reified I : T> doWhen(
noinline filter: suspend (state: State) -> Boolean,
handler: StatesHandler<I, T>
) {
add(filter, handler)
}
fun build() = fsmBuilder(
statesManager,
states.toList()
states.toList().let { list ->
defaultStateHandler ?.let { list + it.holder { true } } ?: list
}
)
}
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
add(I::class, handler)
}
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
addStrict(I::class, handler)
}
fun buildFSM(
block: FSMBuilder.() -> Unit
): StatesMachine = FSMBuilder().apply(block).build()
fun <T : State> buildFSM(
block: FSMBuilder<T>.() -> Unit
): StatesMachine<T> = FSMBuilder<T>().apply(block).build()

View File

@@ -9,25 +9,25 @@ import kotlinx.coroutines.sync.withLock
/**
* Implement this repo if you want to use some custom repo for [DefaultStatesManager]
*/
interface DefaultStatesManagerRepo {
interface DefaultStatesManagerRepo<T : State> {
/**
* Must save [state] as current state of chain with [State.context] of [state]
*/
suspend fun set(state: State)
suspend fun set(state: T)
/**
* Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should
* NOT be removed
*/
suspend fun removeState(state: State)
suspend fun removeState(state: T)
/**
* @return Current list of available and saved states
*/
suspend fun getStates(): List<State>
suspend fun getStates(): List<T>
/**
* @return Current state by [context]
*/
suspend fun getContextState(context: Any): State?
suspend fun getContextState(context: Any): T?
/**
* @return Current state by [context]
@@ -37,36 +37,49 @@ interface DefaultStatesManagerRepo {
/**
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default will be used
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
* this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
* with [endChain] with current state
* @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class DefaultStatesManager(
private val repo: DefaultStatesManagerRepo = InMemoryDefaultStatesManagerRepo(),
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<State>(0)
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
open class DefaultStatesManager<T : State>(
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) : StatesManager<T> {
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
protected val _onStartChain = MutableSharedFlow<T>(0)
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
protected val _onEndChain = MutableSharedFlow<T>(0)
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
private val mapMutex = Mutex()
protected val mapMutex = Mutex()
override suspend fun update(old: State, new: State) = mapMutex.withLock {
val stateByOldContext: State? = repo.getContextState(old.context)
constructor(
repo: DefaultStatesManagerRepo<T>,
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
) : this (
repo,
onUpdateContextsConflictResolver = onContextsConflictResolver
)
override suspend fun update(old: T, new: T) = mapMutex.withLock {
val stateByOldContext: T? = repo.getContextState(old.context)
when {
stateByOldContext != old -> return@withLock
stateByOldContext == null || old.context == new.context -> {
repo.removeState(old)
repo.set(new)
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = repo.getContextState(new.context)
if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
repo.removeState(old)
repo.set(new)
@@ -76,26 +89,30 @@ class DefaultStatesManager(
}
}
override suspend fun startChain(state: State) = mapMutex.withLock {
if (!repo.contains(state.context)) {
override suspend fun startChain(state: T) = mapMutex.withLock {
val stateOnContext = repo.getContextState(state.context)
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
stateOnContext ?.let {
endChainWithoutLock(it)
}
repo.set(state)
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: State) {
protected open suspend fun endChainWithoutLock(state: T) {
if (repo.getContextState(state.context) == state) {
repo.removeState(state)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
override suspend fun endChain(state: T) {
mapMutex.withLock {
endChainWithoutLock(state)
}
}
override suspend fun getActiveStates(): List<State> = repo.getStates()
override suspend fun getActiveStates(): List<T> = repo.getStates()
}

View File

@@ -6,20 +6,20 @@ import dev.inmo.micro_utils.fsm.common.State
* Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive
* functionality
*/
class InMemoryDefaultStatesManagerRepo(
private val map: MutableMap<Any, State> = mutableMapOf()
) : DefaultStatesManagerRepo {
override suspend fun set(state: State) {
class InMemoryDefaultStatesManagerRepo<T : State>(
private val map: MutableMap<Any, T> = mutableMapOf()
) : DefaultStatesManagerRepo<T> {
override suspend fun set(state: T) {
map[state.context] = state
}
override suspend fun removeState(state: State) {
override suspend fun removeState(state: T) {
map.remove(state.context)
}
override suspend fun getStates(): List<State> = map.values.toList()
override suspend fun getStates(): List<T> = map.values.toList()
override suspend fun getContextState(context: Any): State? = map[context]
override suspend fun getContextState(context: Any): T? = map[context]
override suspend fun contains(context: Any): Boolean = map.contains(context)
}

View File

@@ -1,68 +1,17 @@
package dev.inmo.micro_utils.fsm.common.managers
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.fsm.common.StatesManager
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
*
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class InMemoryStatesManager(
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<State>(0)
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
private val contextsToStates = mutableMapOf<Any, State>()
private val mapMutex = Mutex()
override suspend fun update(old: State, new: State) = mapMutex.withLock {
when {
contextsToStates[old.context] != old -> return@withLock
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
contextsToStates[old.context] = new
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = contextsToStates.getValue(new.context)
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
endChainWithoutLock(stateOnNewOneContext)
contextsToStates.remove(old.context)
contextsToStates[new.context] = new
_onChainStateUpdated.emit(old to new)
}
}
}
}
override suspend fun startChain(state: State) = mapMutex.withLock {
if (!contextsToStates.containsKey(state.context)) {
contextsToStates[state.context] = state
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: State) {
if (contextsToStates[state.context] == state) {
contextsToStates.remove(state.context)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
mapMutex.withLock {
endChainWithoutLock(state)
}
}
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
}
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
fun <T: State> InMemoryStatesManager(
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)

View File

@@ -1,6 +1,7 @@
import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
import kotlinx.coroutines.*
sealed interface TrafficLightState : State {
@@ -25,9 +26,9 @@ class PlayableMain {
}
}
val statesManager = InMemoryStatesManager()
val statesManager = DefaultStatesManager<TrafficLightState>()
val machine = buildFSM {
val machine = buildFSM<TrafficLightState> {
strictlyOn<GreenCommon> {
delay(1000L)
YellowCommon(it.context).also(::println)

View File

@@ -5,21 +5,21 @@ import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.pagination.getAll
class KeyValueBasedDefaultStatesManagerRepo(
private val keyValueRepo: KeyValueRepo<Any, State>
) : DefaultStatesManagerRepo {
override suspend fun set(state: State) {
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
private val keyValueRepo: KeyValueRepo<Any, T>
) : DefaultStatesManagerRepo<T> {
override suspend fun set(state: T) {
keyValueRepo.set(state.context, state)
}
override suspend fun removeState(state: State) {
override suspend fun removeState(state: T) {
if (keyValueRepo.get(state.context) == state) {
keyValueRepo.unset(state.context)
}
}
override suspend fun getStates(): List<State> = keyValueRepo.getAll { keys(it) }.map { it.second }
override suspend fun getContextState(context: Any): State? = keyValueRepo.get(context)
override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second }
override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context)
override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context)
}

View File

@@ -1,84 +0,0 @@
package dev.inmo.micro_utils.fsm.repos.common
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.fsm.common.StatesManager
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.micro_utils.repos.pagination.getAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@Deprecated("Replace with DefaultStatesManager and KeyValueBasedDefaultStatesManagerRepo")
class KeyValueBasedStatesManager(
private val keyValueRepo: KeyValueRepo<Any, State>,
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
private val mutex = Mutex()
override suspend fun update(old: State, new: State) {
mutex.withLock {
when {
keyValueRepo.get(old.context) != old -> return@withLock
old.context == new.context || !keyValueRepo.contains(new.context) -> {
keyValueRepo.set(old.context, new)
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = keyValueRepo.get(new.context)!!
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
endChainWithoutLock(stateOnNewOneContext)
keyValueRepo.unset(old.context)
keyValueRepo.set(new.context, new)
_onChainStateUpdated.emit(old to new)
}
}
}
}
}
override suspend fun startChain(state: State) {
if (!keyValueRepo.contains(state.context)) {
keyValueRepo.set(state.context, state)
}
}
private suspend fun endChainWithoutLock(state: State) {
if (keyValueRepo.get(state.context) == state) {
keyValueRepo.unset(state.context)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
mutex.withLock { endChainWithoutLock(state) }
}
override suspend fun getActiveStates(): List<State> {
return keyValueRepo.getAll { keys(it) }.map { it.second }
}
}
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
noinline outToStateTransformer: suspend TargetStateType.() -> State,
) = KeyValueBasedStatesManager(
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
contextToOutTransformer,
stateToOutTransformer,
outToContextTransformer,
outToStateTransformer
)
)

View File

@@ -7,43 +7,12 @@ android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g
kotlin_version=1.5.31
kotlin_coroutines_version=1.5.2
kotlin_serialisation_core_version=1.3.0
kotlin_exposed_version=0.35.2
ktor_version=1.6.4
klockVersion=2.4.6
github_release_plugin_version=2.2.12
uuidVersion=0.3.1
# ANDROID
core_ktx_version=1.6.0
androidx_recycler_version=1.2.1
appcompat_version=1.3.1
android_minSdkVersion=19
android_compileSdkVersion=31
android_buildToolsVersion=31.0.0
dexcount_version=3.0.0
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
# JS NPM
crypto_js_version=4.1.1
# Dokka
dokka_version=1.5.31
# Project data
group=dev.inmo
version=0.7.2
android_code_version=76
version=0.9.22
android_code_version=112

77
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,77 @@
[versions]
kt = "1.6.10"
kt-serialization = "1.3.2"
kt-coroutines = "1.6.1"
jb-compose = "1.1.1"
jb-exposed = "0.37.3"
jb-dokka = "1.6.10"
klock = "2.7.0"
uuid = "0.4.0"
ktor = "1.6.8"
gh-release = "2.2.12"
android-gradle = "7.0.4"
dexcount = "3.0.1"
android-coreKtx = "1.7.0"
android-recyclerView = "1.2.1"
android-appCompat = "1.4.1"
android-espresso = "3.3.0"
android-test = "1.1.2"
android-props-minSdk = "19"
android-props-compileSdk = "32"
android-props-buildTools = "32.0.0"
[libraries]
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
[plugins]
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -12,7 +12,7 @@ kotlin {
dependencies {
api internalProject("micro_utils.ktor.common")
api internalProject("micro_utils.coroutines")
api "io.ktor:ktor-client-core:$ktor_version"
api libs.ktor.client
}
}
}

View File

@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.ws
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readBytes
import kotlinx.coroutines.flow.Flow
@@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
): Flow<T> {
val correctedUrl = url.asCorrectWebSocketUrl
@@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
do {
val reconnect = try {
safely {
ws(correctedUrl) {
ws(correctedUrl, requestBuilder) {
for (received in incoming) {
when (received) {
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
@@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow(
url,
checkReconnection
checkReconnection,
requestBuilder
) {
serialFormat.decodeDefault(deserializer, it)
}

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.client.request.forms.InputProvider
expect suspend fun MPPFile.inputProvider(): InputProvider

View File

@@ -1,16 +1,20 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.serialization.*
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
class UnifiedRequester(
private val client: HttpClient = HttpClient(),
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
val client: HttpClient = HttpClient(),
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) {
suspend fun <ResultType> uniget(
url: String,
@@ -31,11 +35,66 @@ class UnifiedRequester(
resultDeserializer: DeserializationStrategy<ResultType>
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
mppFile: MPPFile,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
fun <T> createStandardWebsocketFlow(
url: String,
checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
checkReconnection: (Throwable?) -> Boolean,
deserializer: DeserializationStrategy<T>,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder)
fun <T> createStandardWebsocketFlow(
url: String,
deserializer: DeserializationStrategy<T>,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
}
val defaultRequester = UnifiedRequester()
@@ -69,3 +128,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
}.let {
serialFormat.decodeDefault(resultDeserializer, it)
}
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
url,
formData = formData {
append(
"bytes",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, mimetype)
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
}
) {
requestBuilder()
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
filename: String,
otherData: BodyPair<BodyType>,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
filename,
inputProvider,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile.filename.string,
mppFile.inputProvider(),
resultDeserializer,
mimetype,
additionalParametersBuilder,
dataHeadersBuilder,
requestBuilder,
serialFormat
)
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)

View File

@@ -0,0 +1,19 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
expect suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
): TemporalFileId
suspend fun UnifiedRequester.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
): TemporalFileId = client.tempUpload(
fullTempUploadDraftPath, file, onUpload
)

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.*
import io.ktor.client.request.forms.InputProvider
import io.ktor.utils.io.core.ByteReadPacket
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
InputProvider(it.size.toLong()) {
ByteReadPacket(it)
}
}

View File

@@ -0,0 +1,58 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import kotlinx.coroutines.*
import org.w3c.xhr.*
suspend fun tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (Long, Long) -> Unit
): TemporalFileId {
val formData = FormData()
val answer = CompletableDeferred<TemporalFileId>()
formData.append(
"data",
file
)
val request = XMLHttpRequest()
request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = {
onUpload(it.loaded.toLong(), it.total.toLong())
}
request.onload = {
if (request.status == 200.toShort()) {
answer.complete(TemporalFileId(request.responseText))
} else {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
}
request.onerror = {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
request.open("POST", fullTempUploadDraftPath, true)
request.send(formData)
val handle = currentCoroutineContext().job.invokeOnCompletion {
runCatching {
request.abort()
}
}
return runCatching {
answer.await()
}.also {
handle.dispose()
}.getOrThrow()
}
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.client.request.forms.InputProvider
import io.ktor.utils.io.streams.asInput
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
inputStream().asInput()
}

View File

@@ -0,0 +1,39 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import io.ktor.client.features.onUpload
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import java.net.URLConnection
internal val MPPFile.mimeType: String
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (Long, Long) -> Unit
): TemporalFileId {
val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData<String>(
fullTempUploadDraftPath,
formData = formData {
append(
"data",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, file.mimeType)
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
}
)
}
) {
onUpload(onUpload)
}
return TemporalFileId(fileId)
}

View File

@@ -10,8 +10,10 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion"
api internalProject("micro_utils.common")
api libs.kt.serialization.cbor
api libs.klock
api libs.uuid
}
}
}

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.ktor.common
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
const val DefaultTemporalFilesSubPath = "temp_upload"
@Serializable
@JvmInline
value class TemporalFileId(val string: String)

View File

@@ -16,10 +16,10 @@ kotlin {
jvmMain {
dependencies {
api "io.ktor:ktor-server:$ktor_version"
api "io.ktor:ktor-server-cio:$ktor_version"
api "io.ktor:ktor-server-host-common:$ktor_version"
api "io.ktor:ktor-websockets:$ktor_version"
api libs.ktor.server
api libs.ktor.server.cio
api libs.ktor.server.host.common
api libs.ktor.websockets
}
}
}

View File

@@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.featureOrNull
import io.ktor.application.install
import io.ktor.http.URLProtocol
import io.ktor.http.cio.websocket.*
import io.ktor.routing.Route
import io.ktor.websocket.webSocket
import io.ktor.routing.application
import io.ktor.websocket.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.serialization.SerializationStrategy
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
if (incoming.tryReceive() != null) {
close()
throw CorrectCloseException
}
}
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
converter: (T) -> StandardKtorSerialInputData
protocol: URLProtocol = URLProtocol.WS,
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
) {
webSocket(suburl) {
application.apply {
featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets)
}
webSocket(suburl, protocol.name) {
safely {
flow.collect {
send(converter(it))
converter(it) ?.let { data ->
send(data)
}
}
}
}
@@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
serializer: SerializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
protocol: URLProtocol = URLProtocol.WS,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(
suburl,
flow
) {
serialFormat.encodeDefault(serializer, it)
}
flow,
protocol,
converter = if (filter == null) {
{
serialFormat.encodeDefault(serializer, it)
}
} else {
{
if (filter(it)) {
serialFormat.encodeDefault(serializer, it)
} else {
null
}
}
}
)

View File

@@ -1,28 +1,39 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.*
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.request.receive
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.response.respondBytes
import io.ktor.routing.Route
import io.ktor.util.asStream
import io.ktor.util.cio.writeChannel
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.*
import io.ktor.websocket.WebSocketServerSession
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.*
import java.io.File
import java.io.File.createTempFile
class UnifiedRouter(
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
) {
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
serializer: SerializationStrategy<T>
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
serializer: SerializationStrategy<T>,
protocol: URLProtocol = URLProtocol.WS,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
answerSerializer: SerializationStrategy<T>,
@@ -81,6 +92,11 @@ class UnifiedRouter(
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
}
}
companion object {
val default
get() = defaultUnifiedRouter
}
}
val defaultUnifiedRouter = UnifiedRouter()
@@ -104,6 +120,139 @@ suspend fun <T> ApplicationCall.uniload(
)
}
suspend fun ApplicationCall.uniloadMultipart(
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
) = safely {
val multipartData = receiveMultipart()
var resultInput: Input? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
when (it.name) {
"bytes" -> resultInput = it.provider()
else -> onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
}
}
resultInput ?: error("Bytes has not been received")
}
suspend fun <T> ApplicationCall.uniloadMultipart(
deserializer: DeserializationStrategy<T>,
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
): Pair<Input, T> {
var data: Optional<T>? = null
val resultInput = uniloadMultipart(
onFormItem,
{
if (it.name == "data") {
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
} else {
onCustomFileItem(it)
}
},
onBinaryContent
)
val completeData = data ?: error("Data has not been received")
return resultInput to (completeData.dataOrNull().let { it as T })
}
suspend fun <T> ApplicationCall.uniloadMultipartFile(
deserializer: DeserializationStrategy<T>,
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
) = safely {
val multipartData = receiveMultipart()
var resultInput: MPPFile? = null
var data: Optional<T>? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
when (it.name) {
"bytes" -> {
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
resultInput = MPPFile.createTempFile(
name.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.provider().asStream().copyTo(fileStream)
}
}
}
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
else -> onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
}
}
val completeData = data ?: error("Data has not been received")
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
}
suspend fun ApplicationCall.uniloadMultipartFile(
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
) = safely {
val multipartData = receiveMultipart()
var resultInput: MPPFile? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
if (it.name == "bytes") {
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
resultInput = MPPFile.createTempFile(
name.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.provider().asStream().copyTo(fileStream)
}
}
} else {
onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
}
}
resultInput ?: error("Bytes has not been received")
}
suspend fun ApplicationCall.getParameterOrSendError(
field: String
) = parameters[field].also {

View File

@@ -1,7 +1,9 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.*
import kotlin.random.Random
@@ -9,16 +11,57 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
block: Application.() -> Unit
): TEngine {
val env = applicationEngineEnvironment {
): TEngine = embeddedServer(
engine,
applicationEngineEnvironment {
module(block)
connector {
this@connector.host = host
this@connector.port = port
this.host = host
this.port = port
}
}
return embeddedServer(engine, env)
additionalEngineEnvironmentConfigurator()
},
additionalConfigurationConfigurator
)
/**
* Create server with [CIO] server engine without starting of it
*
* @see ApplicationEngine.start
*/
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
block: Application.() -> Unit
): CIOApplicationEngine = createKtorServer(
CIO,
host,
port,
additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator,
block
)
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
configurators: List<KtorApplicationConfigurator>
): TEngine = createKtorServer(
engine,
host,
port,
additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator
) {
configurators.forEach { it.apply { configure() } }
}
/**
@@ -29,5 +72,7 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
block: Application.() -> Unit
): ApplicationEngine = createKtorServer(CIO, host, port, block)
configurators: List<KtorApplicationConfigurator>,
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators)

View File

@@ -0,0 +1,132 @@
package dev.inmo.micro_utils.ktor.server
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.streamProvider
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.routing.post
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.FileTime
class TemporalFilesRoutingConfigurator(
private val subpath: String = DefaultTemporalFilesSubPath,
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
) : ApplicationRoutingConfigurator.Element {
interface TemporalFilesUtilizer {
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
companion object : TemporalFilesUtilizer {
class ByTimerUtilizer(
private val removeMillis: Long,
private val scope: CoroutineScope
) : TemporalFilesUtilizer {
override fun start(
filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId>
): Job = scope.launchSafelyWithoutExceptions {
while (isActive) {
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
}
if (filesWithCreationInfo.isEmpty()) {
delay(removeMillis)
continue
}
var min = filesWithCreationInfo.first()
for (fileWithCreationInfo in filesWithCreationInfo) {
if (fileWithCreationInfo.second < min.second) {
min = fileWithCreationInfo
}
}
delay(System.currentTimeMillis() - (min.second + removeMillis))
filesMutex.withLock {
filesMap.remove(min.first)
} ?.delete()
}
}
}
override fun start(
filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId>
): Job = Job()
}
}
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
private val temporalFilesMutex = Mutex()
private val filesFlow = MutableSharedFlow<TemporalFileId>()
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
override fun Route.invoke() {
post(subpath) {
unifiedRouter.apply {
val multipart = call.receiveMultipart()
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
var part = multipart.readPart()
while (part != null) {
if (part is PartData.FileItem) {
break
}
part = multipart.readPart()
}
part ?.let {
if (it is PartData.FileItem) {
val fileId = TemporalFileId(uuid4().toString())
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
outputStream().use { outputStream ->
it.streamProvider().use {
it.copyTo(outputStream)
}
}
deleteOnExit()
}
}
}
fileInfo ?.also { (fileId, file) ->
temporalFilesMutex.withLock {
temporalFilesMap[fileId] = file
}
call.respond(fileId.string)
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
} ?: call.respond(HttpStatusCode.BadRequest)
}
}
}
suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
temporalFilesMutex.withLock {
temporalFilesMap.remove(temporalFileId)
}
}
fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
temporalFilesMap.remove(temporalFileId)
}
}

View File

@@ -4,8 +4,8 @@ buildscript {
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization
}
}
@@ -16,11 +16,16 @@ plugins {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
implementation libs.kt.stdlib
implementation libs.kt.serialization
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-java:$ktor_version"
implementation libs.ktor.client
implementation libs.ktor.client.java
}
mainClassName="MainKt"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
package dev.inmo.micro_utils.language_codes
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,52 @@
import requests
from bs4 import BeautifulSoup
import pandas as pd
import itertools
def fix_name(category, raw_name):
splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+')
out1 = ""
for s in splitted:
out1 += s.capitalize()
result = ""
if out1[0].isdigit():
result += category[0].capitalize()
result += out1
else:
result += out1
return result
if __name__ == '__main__':
df = pd.read_html(open('table.html', 'r'))
mimes = []
for row in df[0].iterrows():
mime = row[1][1]
mime_category = mime.split('/', 1)[0]
mime_name = mime.split('/', 1)[1]
mimes.append({
'mime_category': mime_category,
'mime_name': mime_name,
})
# codegen
mimes.sort(key=lambda x: x['mime_category'])
grouped = itertools.groupby(mimes, lambda x: x['mime_category'])
code = ''
code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n'
code2 += ' KnownMimeTypes.Any,\n'
for key, group in grouped:
group_name = key.capitalize()
code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name
code += ' @Serializable(MimeTypeSerializer::class)\n object Any: %s ("%s/*")\n' % (group_name, key)
for mime in group:
name = fix_name(mime['mime_category'], mime['mime_name'])
code += ' @Serializable(MimeTypeSerializer::class)\n object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name'])
code2 += ' KnownMimeTypes.%s.%s,\n' % (group_name, name)
code += '}\n\n'
code2 += ')\n'
with open('out1.txt', 'w') as file:
file.write(code)
with open('out2.txt', 'w') as file:
file.write(code2)

View File

@@ -1,3 +1,5 @@
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
package dev.inmo.micro_utils.mime_types
import kotlinx.serialization.Serializable

View File

@@ -26,7 +26,6 @@ kotlin {
apply from: "$defaultAndroidSettingsPresetPath"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -4,7 +4,13 @@ project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm()
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
sourceSets {
commonMain {
@@ -28,7 +34,6 @@ kotlin {
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -4,7 +4,13 @@ project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm()
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js (IR) {
browser()
nodejs()
@@ -17,7 +23,7 @@ kotlin {
commonMain {
dependencies {
implementation kotlin('stdlib')
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
implementation libs.kt.serialization
}
}
commonTest {
@@ -40,8 +46,8 @@ kotlin {
androidTest {
dependencies {
implementation kotlin('test-junit')
implementation "androidx.test.ext:junit:$test_ext_junit_version"
implementation "androidx.test.espresso:espresso-core:$espresso_core"
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
}
@@ -50,7 +56,6 @@ kotlin {
apply from: "$defaultAndroidSettingsPresetPath"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -0,0 +1,72 @@
project.version = "$version"
project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js (IR) {
browser()
nodejs()
}
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
implementation libs.kt.serialization
implementation compose.runtime
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
dependencies {
implementation compose.desktop.currentOs
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation compose.web.core
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
implementation kotlin('test-junit')
}
}
androidTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
}
}
apply from: "$defaultAndroidSettingsPresetPath"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -5,3 +5,13 @@ plugins {
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
}
}
}
}

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.pagination
import dev.inmo.micro_utils.common.intersect
import kotlin.math.ceil
import kotlin.math.floor
@@ -9,7 +10,7 @@ import kotlin.math.floor
* If you want to request something, you should use [SimplePagination]. If you need to return some result including
* pagination - [PaginationResult]
*/
interface Pagination {
interface Pagination : ClosedRange<Int> {
/**
* Started with 0.
* Number of page inside of pagination. Offset can be calculated as [page] * [size]
@@ -20,6 +21,17 @@ interface Pagination {
* Size of current page. Offset can be calculated as [page] * [size]
*/
val size: Int
override val start: Int
get() = page * size
override val endInclusive: Int
get() = start + size - 1
}
fun Pagination.intersect(
other: Pagination
): Pagination? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
PaginationByIndexes(it.first, it.second)
}
/**
@@ -32,7 +44,7 @@ inline val Pagination.isFirstPage
* First number in index of objects. It can be used as offset for databases or other data sources
*/
val Pagination.firstIndex: Int
get() = page * size
get() = start
/**
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
@@ -41,7 +53,7 @@ val Pagination.firstIndex: Int
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20
*/
val Pagination.lastIndexExclusive: Int
get() = firstIndex + size
get() = endInclusive + 1
/**
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
@@ -50,7 +62,7 @@ val Pagination.lastIndexExclusive: Int
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19.
*/
val Pagination.lastIndex: Int
get() = lastIndexExclusive - 1
get() = endInclusive
/**
* Calculates pages count for given [datasetSize]

View File

@@ -16,6 +16,16 @@ suspend fun <T> getAll(
return results.toList()
}
suspend fun <T, R> R.getAllBy(
initialPagination: Pagination = FirstPagePagination(),
paginationMapper: R.(PaginationResult<T>) -> Pagination?,
block: suspend R.(Pagination) -> PaginationResult<T>
): List<T> = getAll(
initialPagination,
{ paginationMapper(it) },
{ block(it) }
)
suspend fun <T> getAllWithNextPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
@@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging(
block
)
suspend fun <T, R> R.getAllByWithNextPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend R.(Pagination) -> PaginationResult<T>
): List<T> = getAllWithNextPaging(
initialPagination,
{ block(it) }
)
suspend fun <T> getAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
@@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging(
{ it.currentPageIfNotEmpty() },
block
)
suspend fun <T, R> R.getAllByWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend R.(Pagination) -> PaginationResult<T>
): List<T> = getAllWithCurrentPaging(
initialPagination,
{ block(it) }
)

View File

@@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
size.toLong()
)
}
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
is List<T> -> optionallyReverse(reverse)
is Set<T> -> optionallyReverse(reverse)
else -> if (reverse) {
reversed()
} else {
this
}
}
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
reversed()
} else {
this
}
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
reversed().toSet()
} else {
this
}
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
Array(size) {
get(lastIndex - it)
}
} else {
this
}

View File

@@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.*
* Example:
*
* * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|`
* * `|__f__l_|` will be transformed to `|__f__l_|`
* * `|__f__l_|` will be transformed to `|_f__l__|`
*
* @return Reversed version of this [Pagination]
*/
@@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination {
* Shortcut for [reverse]
*/
fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong())
fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) {
reverse(objectsCount)
} else {
this
}
fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) {
reverse(objectsCount)
} else {
this
}

View File

@@ -14,7 +14,7 @@ kotlin {
}
jvmMain {
dependencies {
api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
api libs.jb.exposed
}
}
}

View File

@@ -15,8 +15,8 @@ kotlin {
jvmMain {
dependencies {
api "io.ktor:ktor-server:$ktor_version"
api "io.ktor:ktor-server-host-common:$ktor_version"
api libs.ktor.server
api libs.ktor.server.host.common
}
}
}

View File

@@ -1,5 +1,4 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
@@ -69,8 +68,19 @@ publishing {
}
}
}
signing {
useGpgCmd()
sign publishing.publications
if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing'
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
}

View File

@@ -1 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}

View File

@@ -10,10 +10,10 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api libs.kt.coroutines
api internalProject("micro_utils.pagination.common")
api "com.benasher44:uuid:$uuidVersion"
api libs.uuid
}
}
@@ -24,7 +24,7 @@ kotlin {
}
androidMain {
dependencies {
api "androidx.core:core-ktx:$core_ktx_version"
api libs.android.coreKtx
api internalProject("micro_utils.common")
api internalProject("micro_utils.coroutines")
}

View File

@@ -13,12 +13,12 @@ interface VersionsRepo<T> : Repo {
* By default, instance of this interface will check that version of table with name [tableName] is less than
* [version] or is absent
*
* * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
* until database version will be equal to [version]
* In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less
* than parameter [version] or null, it will increase version one-by-one until database version will be equal to
* [version]
*
* @param version Current version of table
* @param onCreate This callback will be called in case when table have no information about table
* @param onCreate This callback will be called in case when repo have no information about table
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
* "to" will always be greater than "from"
*/

View File

@@ -0,0 +1,27 @@
package dev.inmo.micro_utils.repos
import android.database.Cursor
class CursorIterator(
private val c: Cursor
) : Iterator<Cursor> {
private var i = 0
init {
c.moveToFirst()
}
override fun hasNext(): Boolean {
return i < c.count
}
override fun next(): Cursor {
i++
return if (c.moveToNext()) {
c
} else {
throw NoSuchElementException()
}
}
}
operator fun Cursor.iterator(): CursorIterator = CursorIterator(this)

View File

@@ -6,13 +6,15 @@ import dev.inmo.micro_utils.repos.*
import kotlinx.coroutines.flow.*
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
helper: StandardSQLHelper
helper: StandardSQLHelper,
replyInFlows: Int = 0,
extraBufferCapacityInFlows: Int = 64
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper),
StandardCRUDRepo<ObjectType, IdType, InputValueType> {
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64)
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyInFlows, extraBufferCapacityInFlows)
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
@@ -102,4 +104,4 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
}
override suspend fun count(): Long = helper.blockingReadableTransaction { select(tableName).use { it.count.toLong() } }
}
}

View File

@@ -143,7 +143,12 @@ class OneToManyAndroidRepo<Key, Value>(
}.toLong()
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
selectDistinct(
tableName,
columns = valueColumnArray,
selection = "$idColumnName=?",
selectionArgs = arrayOf(k.keyAsString())
).use {
it.count
}
}.toLong()

View File

@@ -10,19 +10,26 @@ import org.jetbrains.exposed.sql.transactions.transaction
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
flowsChannelsSize: Int = 0,
tableName: String = ""
tableName: String = "",
replyCacheInFlows: Int = 0
) :
AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName),
ExposedCRUDRepo<ObjectType, IdType>,
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
{
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(flowsChannelsSize)
protected val _newObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val _updatedObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val _deletedObjectsIdsFlow = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
@Deprecated("Renamed", ReplaceWith("_newObjectsFlow"))
protected val newObjectsChannel = _newObjectsFlow
@Deprecated("Renamed", ReplaceWith("_updatedObjectsFlow"))
protected val updateObjectsChannel = _updatedObjectsFlow
@Deprecated("Renamed", ReplaceWith("_deletedObjectsIdsFlow"))
protected val deleteObjectsIdsChannel = _deletedObjectsIdsFlow
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
override val newObjectsFlow: Flow<ObjectType> = _newObjectsFlow.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = _updatedObjectsFlow.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow()
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
@@ -42,7 +49,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
return transaction(db = database) {
values.map { value -> createWithoutNotification(value) }
}.onEach {
newObjectsChannel.emit(it)
_newObjectsFlow.emit(it)
}
}
@@ -73,7 +80,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
onBeforeUpdate(listOf(id to value))
return updateWithoutNotification(id, value).also {
if (it != null) {
updateObjectsChannel.emit(it)
_updatedObjectsFlow.emit(it)
}
}
}
@@ -84,16 +91,25 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
values.map { (id, value) -> updateWithoutNotification(id, value) }
}.filterNotNull()
).onEach {
updateObjectsChannel.emit(it)
_updatedObjectsFlow.emit(it)
}
}
protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
override suspend fun deleteById(ids: List<IdType>) {
onBeforeDelete(ids)
transaction(db = database) {
deleteWhere(null, null) {
val deleted = deleteWhere(null, null) {
selectByIds(ids)
}
if (deleted == ids.size) {
ids
} else {
ids.filter {
select { selectById(it) }.limit(1).none()
}
}
}.forEach {
_deletedObjectsIdsFlow.emit(it)
}
}
}

View File

@@ -19,8 +19,8 @@ open class ExposedKeyValueRepo<Key, Value>(
valueColumnAllocator,
tableName
) {
private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
private val _onValueRemoved = MutableSharedFlow<Key>()
protected val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
protected val _onValueRemoved = MutableSharedFlow<Key>()
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()

Some files were not shown because too many files have changed in this diff Show More