From db7a85fb7bdb993a1ab9771d454891b6726c9652 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 7 Sep 2022 11:45:06 +0300 Subject: [PATCH] binary command protocol minimalistic test --- build.gradle.kts | 11 +- kotlin-js-store/yarn.lock | 147 +++++++++++++++- .../kotlin/net.sergeych.parsec3/Adapter.kt | 165 ++++++++++++++++++ .../net.sergeych.parsec3/AdapterDelegate.kt | 22 +++ .../net.sergeych.parsec3/CommandDescriptor.kt | 20 +++ .../net.sergeych.parsec3/CommandHost.kt | 62 +++++++ .../kotlin/net.sergeych.parsec3/KVStorage.kt | 20 ++- .../kotlin/net.sergeych.parsec3/Package.kt | 28 +++ .../kotlin/net.sergeych.parsec3/errors.kt | 27 +++ src/commonTest/kotlin/parsec3/AdapterTest.kt | 49 ++++++ .../kotlin}/parsec3/MemoryKVStorageTest.kt | 18 +- 11 files changed, 548 insertions(+), 21 deletions(-) create mode 100644 src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt create mode 100644 src/commonMain/kotlin/net.sergeych.parsec3/AdapterDelegate.kt create mode 100644 src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt create mode 100644 src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt create mode 100644 src/commonMain/kotlin/net.sergeych.parsec3/Package.kt create mode 100644 src/commonMain/kotlin/net.sergeych.parsec3/errors.kt create mode 100644 src/commonTest/kotlin/parsec3/AdapterTest.kt rename src/{jvmTest/kotlin/net/sergeych => commonTest/kotlin}/parsec3/MemoryKVStorageTest.kt (58%) diff --git a/build.gradle.kts b/build.gradle.kts index 3a74a52..9600a71 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,13 +38,20 @@ kotlin { dependencies { implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-websockets:$ktor_version") - api("net.sergeych:boss-serialization-mp:0.1.3-SNAPSHOT") - implementation("net.sergeych:mp_stools:[1.3.2-SNAPSHOT,)") + api("net.sergeych:unikrypto:1.2.0-SNAPSHOT") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3") + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-websockets:$ktor_version") + api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") + api("net.sergeych:boss-serialization-mp:[0.1.3-SNAPSHOT,)") + api("net.sergeych:mp_stools:1.2.3-SNAPSHOT") + } } val commonTest by getting { dependencies { implementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") } } val jvmMain by getting { diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 0688268..9bf9443 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -316,6 +316,13 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -341,6 +348,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base64id@2.0.0, base64id@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" @@ -351,6 +363,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bn.js@^4.0.0, bn.js@^4.1.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + body-parser@^1.19.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" @@ -391,6 +408,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -411,6 +433,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.4.2: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -434,6 +464,15 @@ caniuse-lite@^1.0.30001370: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001383.tgz#aecf317ccd940690725ae3ae4f28293c5fb8050e" integrity sha512-swMpEoTp5vDoGBZsYZX7L7nXHe6dsHxi9o6/LKf/f0LukVtnrxly5GVb/fWdCDTqi/yw6Km6tiJ0pmBacm0gbg== +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -480,6 +519,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -487,6 +533,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -622,6 +673,15 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diffie-hellman@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + dom-serialize@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -719,6 +779,11 @@ escape-string-regexp@4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -779,6 +844,11 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== +fastestsmallesttextencoderdecoder@^1.0.14: + version "1.0.22" + resolved "https://registry.yarnpkg.com/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz#59b47e7b965f45258629cc6c127bf783281c5e93" + integrity sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -859,6 +929,14 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +gently-copy@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/gently-copy/-/gently-copy-3.2.0.tgz#6f3b9061c00db6d9bed734b728d93358d1137105" + integrity sha512-IBLU4rCffg0Dvq3/7KyiPionCCdEdKnyfe94c00C8+VbgzIS2J9L2jHdLchG9sn8lDqBGzbvfuYVZB/ZlffS7g== + dependencies: + chalk "^2.4.2" + shelljs "^0.8.3" + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -897,7 +975,7 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3, glob@^7.1.7: +glob@^7.0.0, glob@^7.1.3, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -919,6 +997,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -980,6 +1063,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1001,6 +1089,11 @@ inherits@2, inherits@2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -1090,6 +1183,11 @@ js-yaml@4.1.0: dependencies: argparse "^2.0.1" +jsbn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -1232,6 +1330,14 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -1528,7 +1634,7 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -randombytes@^2.1.0: +randombytes@^2.0.0, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -1557,6 +1663,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" @@ -1586,7 +1699,7 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -1664,6 +1777,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shelljs@^0.8.3: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -1778,6 +1900,13 @@ supports-color@8.1.1, supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1858,6 +1987,18 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== +unicrypto@1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/unicrypto/-/unicrypto-1.14.0.tgz#eee5f2d88d33bb6ba774c8395edf3f4e2a4a68dc" + integrity sha512-NNaMM2Has6Dzk0OAxhR/OfocTKPgA4TbseKqXZ2A7Kb1gcBRN+He7wuxO6QdDOP7tE5SuXVf9C8R9xeswi/0MQ== + dependencies: + buffer "^5.4.2" + diffie-hellman "^5.0.3" + fastestsmallesttextencoderdecoder "^1.0.14" + gently-copy "^3.2.0" + jsbn "^1.1.0" + randombytes "^2.1.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt b/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt new file mode 100644 index 0000000..9e37077 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt @@ -0,0 +1,165 @@ +package channel + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import net.sergeych.boss_serialization.BossDecoder +import net.sergeych.boss_serialization_mp.BossEncoder +import net.sergeych.boss_serialization_mp.decodeBoss +import net.sergeych.cloudoc.api.ApiError.BAD_RESPONSE_PACKAGE +import net.sergeych.cloudoc.api.ApiError.UNKNOWN_ERROR +import net.sergeych.cloudoc.api.ApiException +import net.sergeych.cloudoc.api.Package +import net.sergeych.mp_logger.LogTag +import net.sergeych.mp_logger.debug +import net.sergeych.mp_logger.exception +import net.sergeych.mp_logger.warning +import net.sergeych.mptools.toDump + +/** + * Create adapter, an interface to provide local API commands and invoke remote API commands + * asynchronously and concurrently. To implement adapter over some protocol you need: + * + * - create some class T that will hold the "state" of the API, e.g. session. Use `Unit` for stateless + * - create an CommandHost class and fill it with commands to be executed by a local party (could be empty). + * - provide a method that will transmit binary frames to a remote + * - for any incoming binary frame call [Adapter.receiveFrame]. + * + * Here is the sample of short-circuit pair of adapters: + * + * ``` + * val ch12 = Channel() + * val ch21 = Channel() + * + * val api1 = Api1() + * val api2 = Api2() + * + * // this interface is provided by api1 locally + * api1.on(api1.foo) { + * it + "foo" + * } + * // and that by api2 locally + * api2.on(api2.bar) { + * it + "bar" + * } + * + * // respective adapters that send to a channel: + * val a1 = Adapter(Unit,api1) { ch12.send(it) } + * val a2 = Adapter(Unit,api2) { ch21.send(it) } + * + * // pumps to load frames from the respecive channel and pass them to the adapter: + * launch { for( b in ch12) a2.receiveFrame(b) } + * launch { for( b in ch21) a1.receiveFrame(b) } + * + * // note that adapter `a1` is exepcted to provide `Api2` and vice versa: + * assertEquals("123bar", a1.invokeCommand(api2.bar, "123")) + * assertEquals("321foo", a2.invokeCommand(api1.foo, "321")) + * + * ch12.cancel() // to close channel pump + * ch21.cancel() + * ``` + * + * See [CommandHost] class documentation to learn how to declare API interfaces in a compile time type safe + * manner. + * + * @param instance any instance that represent the state of the interface. Could be `Unit` for stateless. + * @param commandHost the Api __this adapter provides to a remote__. It differs from the interface expected on the + * remote side. + * @param sendEncoded a method that performs actual sending of the packed binary frame to the remote side + */ +open class Adapter( + private val instance: T, + private val commandHost: CommandHost, + private val sendEncoded: suspend (data: ByteArray) -> Unit, +) : LogTag("ADPTR") { + + private val completions = mutableMapOf>() + private var lastId = 1 + private val access = Mutex() + + + /** + * Call the remote party for a type command. See [CommandHost] on how to declare and implement + * such commands in parsec3. Suspends until receiving answer from a remote party. + * + * @param ca command descriptor, provided by [CommandHost.command], usually, it should be a val in the + * [CommandHost] or derived instance. + * @param args command specific args of any serializable type + * @return value from remote partm any serializable type. + */ + @Suppress("UNCHECKED_CAST") + suspend fun invokeCommand(ca: CommandDescriptor, args: A = Unit as A): R { + var myId = -1 + return CompletableDeferred().also { dr -> + sendPackage( + access.withLock { + debug { "calling $lastId:${ca.name}($args)" } + completions[lastId] = dr + myId = lastId + Package.Command(lastId++, ca.name, BossEncoder.encode(ca.ass, args)) + } + ) + }.await().let { + debug { "result $myId:$it" } + BossDecoder.decodeFrom(ca.rss, it) + } + } + + private suspend fun processIncomingPackage(pe: Package) { + when (pe) { + is Package.Command -> { + try { + val handler = commandHost.handler(pe.name) + val result = handler.invoke(instance, pe.args) + sendPackage( + Package.Response( + pe.id, result + ) + ) + } catch (ae: ApiException) { + sendPackage(Package.Response(pe.id, null, ae.code, ae.text)) + } catch (ex: Throwable) { + ex.printStackTrace() + sendPackage(Package.Response(pe.id, null, UNKNOWN_ERROR, ex.toString())) + } + } + + is Package.Response -> { + val dr = access.withLock { completions.remove(pe.toId) } + if (dr == null) + warning { "response to unregistered toId: ${pe.toId}, ignoring" } + else { + if (pe.result != null) + dr.complete(pe.result) + else + dr.completeExceptionally( + pe.errorCode?.let { ApiException(it, pe.errorText) } + ?: ApiException(BAD_RESPONSE_PACKAGE) + ) + } + } + } + } + + private suspend fun sendPackage(pe: Package) { + sendEncoded(BossEncoder.encode(pe)) + } + + /** + * Provide an incoming frame to the adapter. Consumer software receives binary blocks from whatever + * protocol it uses (say, UDP) and feed them to this method. + */ + suspend fun receiveFrame(data: ByteArray) { + try { + processIncomingPackage(data.decodeBoss()) + } catch (x: Exception) { + exception { "unexpected error processing frame: \n${data.toDump()}" to x } + } + } + + companion object { + val format = Json { prettyPrint = true } + } +} + diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/AdapterDelegate.kt b/src/commonMain/kotlin/net.sergeych.parsec3/AdapterDelegate.kt new file mode 100644 index 0000000..b5e72ed --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/AdapterDelegate.kt @@ -0,0 +1,22 @@ +package net.sergeych.parsec3 + +import channel.CommandDescriptor +import kotlin.reflect.KProperty +import kotlin.reflect.KType + +/** + * Delegate to generate proper [CommandDescriptor] instances. See [CommandHost] for usage sample, and + * [Adapter] form more information/ + */ +class AdapterDelegate( + val overrideName: String? = null, + val ass: KType, + val rss: KType, +) { + operator fun getValue(thisRef: Any?, property: KProperty<*>): CommandDescriptor { + return CommandDescriptor( + overrideName ?: property.name, + ass, rss + ) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt b/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt new file mode 100644 index 0000000..59d4570 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt @@ -0,0 +1,20 @@ +package channel + +import kotlin.reflect.KType + +class CommandDescriptor( + val name: String, + val ass: KType, + val rss: KType, +) { + suspend operator fun invoke(adapter: Adapter, args: A): R = + adapter.invokeCommand(this, args) + + @Suppress("UNCHECKED_CAST") + suspend operator fun invoke(adapter: Adapter): R = adapter.invokeCommand(this,Unit as A) + + operator fun invoke(commandHost: CommandHost, block: suspend I.(A)->R) { + commandHost.on(this, block) + } +} + diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt b/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt new file mode 100644 index 0000000..f865675 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt @@ -0,0 +1,62 @@ +package channel + +import net.sergeych.boss_serialization.BossDecoder +import net.sergeych.boss_serialization_mp.BossEncoder +import net.sergeych.cloudoc.api.ApiError +import net.sergeych.parsec3.AdapterDelegate +import kotlin.reflect.typeOf + +/** + * The class that provides parsec3 commands for remote callers. Could be used as is oar as a base + * class. For more information see [Adapter] class. The basic usage pattern is: + * ~~~ + * class Api1: CommandHost() { + * // create command `foo` that takes a string argument and + * // returns a string: + * val foo by command() + * } + * ~~~ + * + * Then somewhere else (usually before initializing network connection) provide implementation for + * the declared commands: + * ~~~ + * val api1 = Api1() + * val api2 = Api2() + * api1.on(api1.foo) { + * it + "foo" + * } + * ~~~ + * + * The good strategy is to create _one Api implementation_, if need using session type [T] to provide + * state-aware behavior, and share it when creating adapters on, say, incoming connections. + * + * @param T the type of the `state` instance used to hold state, use `Unit` for stateless interfaces + */ +open class CommandHost { + private val handlers = mutableMapOf ByteArray>() + + /** + * Provide implementation for a specific command in type-safe compile-time checked manner. the command + * should be declared with [command] invocation. + */ + fun on(ca: CommandDescriptor, block: suspend T.(A) -> R) { + handlers[ca.name] = {args -> + val decodedArgs = BossDecoder.decodeFrom(ca.ass, args) + BossEncoder.encode(ca.rss, block(decodedArgs)) + } + } + + fun handler(name: String) = handlers.get(name) ?: ApiError.NOT_FOUND.raise("command not found") + + /** + * Provide a command delegate that creates type-safe command descriptor containint command name and + * types of it arguments and return value. + */ + inline fun command(name: String? = null): AdapterDelegate { + return AdapterDelegate( + name, + typeOf(), + typeOf() + ) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/KVStorage.kt b/src/commonMain/kotlin/net.sergeych.parsec3/KVStorage.kt index 0f0c65a..aba32f4 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/KVStorage.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/KVStorage.kt @@ -2,17 +2,23 @@ package net.sergeych.parsec3 import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization_mp.BossEncoder -import net.sergeych.boss_serialization_mp.decodeBoss -import net.sergeych.boss_serialization_mp.stored import net.sergeych.mptools.toDump import kotlin.reflect.KProperty import kotlin.reflect.KType import kotlin.reflect.typeOf + +/** + * Generic storage of binary content. PArsec uses boss encoding to store everything in it + * in te convenient way. See [KVStorage.stored] delegate. The [MemoryKVStorage] allows to + * store values in-memory and then connect some permanent stprage to it in a transparent way. + */ interface KVStorage { operator fun get(key: String): ByteArray? operator fun set(key: String, value: ByteArray?) operator fun contains(key: String): Boolean + + val keys: Collection fun clear() { @@ -30,13 +36,9 @@ interface KVStorage { inline operator fun KVStorage.invoke(defaultValue: T,overrideName: String? = null) = KVStorageDelegate(this, typeOf(), defaultValue, overrideName) -inline fun KVStorage.storage(defaultValue: T,overrideName: String? = null) = +inline fun KVStorage.stored(defaultValue: T, overrideName: String? = null) = KVStorageDelegate(this, typeOf(), defaultValue, overrideName) -//inline fun storage(s: KVStorage, defaultValue: T,overrideName: String? = null) = -// KVStorageDelegate(s, typeOf(), defaultValue, overrideName) -// - class KVStorageDelegate( private val storage: KVStorage, private val type: KType, @@ -70,7 +72,9 @@ class KVStorageDelegate( } } - +/** + * Memory storage allows to connect existing in-memory content to some permanent storage on the fly + */ class MemoryKVStorage(copyFrom: KVStorage? = null) : KVStorage { // is used when connected: diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt b/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt new file mode 100644 index 0000000..3a4dda1 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt @@ -0,0 +1,28 @@ +package net.sergeych.cloudoc.api + +import kotlinx.serialization.Serializable + +@Serializable +sealed class Package { + + @Serializable + data class Command(val id: Int, val name: String, val args: ByteArray) : Package() + + @Serializable + data class Response( + val toId: Int, + val result: ByteArray? = null, + val errorCode: ApiError? = null, + val errorText: String? = null, + ) : Package() { + init { + if (result == null && errorCode == null) + throw IllegalArgumentException("either result or errorCode must not be null") + } + } + +// @Serializable +// data class Push(val payload: PushPayload) : Package() +} + + diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/errors.kt b/src/commonMain/kotlin/net.sergeych.parsec3/errors.kt new file mode 100644 index 0000000..638dfd6 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/errors.kt @@ -0,0 +1,27 @@ +package net.sergeych.cloudoc.api + +enum class ApiError { + UNKNOWN_ERROR, + INTERNAL_ERROR, + BAD_RESPONSE_PACKAGE, + NOT_FOUND, + BAD_LOGIN, + OBJECT_ALREADY_EXISTS, + ACCESS_FORBIDDEN, + ILLEGAL_STATE, + NOT_LOGGED_IN, + BAD_PARAMETER, + EMAIL_IN_USE, + NAME_IN_USE; + + fun raise(text: String? = null): Nothing { + throw ApiException(this, text ?: defaultText()) + } + + fun defaultText(): String = name.lowercase().replace('_', ' ') +} + +class ApiException(val code: ApiError, _text: String? = null, cause: Throwable? = null): +Exception(_text ?: code.defaultText(), cause) { + val text by lazy { _text ?: code.defaultText() } +} diff --git a/src/commonTest/kotlin/parsec3/AdapterTest.kt b/src/commonTest/kotlin/parsec3/AdapterTest.kt new file mode 100644 index 0000000..072e3e5 --- /dev/null +++ b/src/commonTest/kotlin/parsec3/AdapterTest.kt @@ -0,0 +1,49 @@ +package parsec3 + +import channel.Adapter +import channel.CommandHost +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class AdapterTest { + + class Api1: CommandHost() { + // create command `foo` that takes a string argument and + // returns a string: + val foo by command() + } + + class Api2: CommandHost() { + + val bar by command() + } + + @Test + fun interconnect() = runTest { + val ch12 = Channel() + val ch21 = Channel() + val api1 = Api1() + val api2 = Api2() + api1.on(api1.foo) { + it + "foo" + } + api2.on(api2.bar) { + it + "bar" + } + + val a1 = Adapter(Unit,api1) { ch12.send(it) } + val a2 = Adapter(Unit,api2) { ch21.send(it) } + launch { for( b in ch12) a2.receiveFrame(b) } + launch { for( b in ch21) a1.receiveFrame(b) } + + assertEquals("123bar", a1.invokeCommand(api2.bar, "123")) + assertEquals("321foo", a2.invokeCommand(api1.foo, "321")) + + ch12.cancel() + ch21.cancel() + + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/net/sergeych/parsec3/MemoryKVStorageTest.kt b/src/commonTest/kotlin/parsec3/MemoryKVStorageTest.kt similarity index 58% rename from src/jvmTest/kotlin/net/sergeych/parsec3/MemoryKVStorageTest.kt rename to src/commonTest/kotlin/parsec3/MemoryKVStorageTest.kt index 5ec5df9..49c90f6 100644 --- a/src/jvmTest/kotlin/net/sergeych/parsec3/MemoryKVStorageTest.kt +++ b/src/commonTest/kotlin/parsec3/MemoryKVStorageTest.kt @@ -1,26 +1,28 @@ -package net.sergeych.parsec3 +package parsec3 import net.sergeych.mptools.toDump -import kotlin.test.* - -import org.junit.jupiter.api.Assertions.* +import net.sergeych.parsec3.MemoryKVStorage +import net.sergeych.parsec3.stored +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull internal class MemoryKVStorageTest { @Test - fun connectToStorage() { - val s = defaultNamedStorage("test11") + fun testStorage() { + val s = MemoryKVStorage() s.clear() println(s) println(s["foo"]) s["foo"] = byteArrayOf(1,2,3) - var bar: String? by s(null) + var bar: String? by s.stored(null) assertNull(bar) bar = "foo" println(s.get("bar")?.toDump()) assertEquals("foo", bar) - var foo: String by s("", "bar") + var foo: String by s.stored("", "bar") assertEquals("foo", foo) // // var i: Int? by s(defaultValue = 17)