From 3bdf1b9e4cad7ee1bb05f6a11efff11aca2ccd47 Mon Sep 17 00:00:00 2001 From: Sergey Panov Date: Fri, 15 Jul 2022 19:09:56 +0700 Subject: [PATCH 1/5] fix apigenerator to apply submitForBinaryData --- .../resources/kotlin-ktor-client/api.mustache | 91 ++++++++++++++++--- sample/mpp-library/src/formData.yaml | 1 + 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/network-generator/src/main/resources/kotlin-ktor-client/api.mustache b/network-generator/src/main/resources/kotlin-ktor-client/api.mustache index d77db82..22b2ac0 100755 --- a/network-generator/src/main/resources/kotlin-ktor-client/api.mustache +++ b/network-generator/src/main/resources/kotlin-ktor-client/api.mustache @@ -18,7 +18,14 @@ import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.serializer import io.ktor.client.call.ReceivePipelineException +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.http.ContentDisposition +import io.ktor.http.HttpHeaders import io.ktor.http.content.TextContent +import io.ktor.http.Headers +import io.ktor.utils.io.core.Input {{#operations}} {{>classes_modifiers}}interface {{classname}} { @@ -30,7 +37,7 @@ import io.ktor.http.content.TextContent {{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}} {{/allParams}}* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} */ - {{^nonPublicApi}}public {{/nonPublicApi}}suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}? = null{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}kotlin.Unit{{/returnType}} + {{^nonPublicApi}}public {{/nonPublicApi}}suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{#isBinary}}Input{{/isBinary}}{{^isBinary}}{{{dataType}}}{{/isBinary}}{{^required}}? = null{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}kotlin.Unit{{/returnType}} {{/operation}} @@ -52,7 +59,8 @@ import io.ktor.http.content.TextContent {{/allParams}}* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} */{{#returnType}} @Suppress("UNCHECKED_CAST"){{/returnType}} - override {{^nonPublicApi}}public {{/nonPublicApi}}{{#isOpen}}open {{/isOpen}}suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}kotlin.Unit{{/returnType}} { + override {{^nonPublicApi}}public {{/nonPublicApi}}{{#isOpen}}open {{/isOpen}}suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{#isBinary}}Input{{/isBinary}}{{^isBinary}}{{{dataType}}}{{/isBinary}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}kotlin.Unit{{/returnType}} { + {{^hasFormParams}} val builder = HttpRequestBuilder() builder.method = HttpMethod.{{httpMethod}} @@ -110,14 +118,6 @@ import io.ktor.http.content.TextContent } {{/bodyParams}} - {{#hasFormParams}} - val formData = FormDataContent(Parameters.build { - {{#formParams}} - {{paramName}}?.let { append("{{baseName}}", it.toString()) } - {{/formParams}} - }) - builder.body = formData - {{/hasFormParams}} with(builder.headers) { append("Accept", "application/json") {{#headerParams}} @@ -141,8 +141,75 @@ import io.ktor.http.content.TextContent } catch (pipeline: ReceivePipelineException) { throw pipeline.cause } - } + {{/hasFormParams}} + {{#hasFormParams}} + val fileName = "image.jpg" + + try { + {{#returnType}}return {{/returnType}}_httpClient.submitFormWithBinaryData { + url { + takeFrom(_basePath) + encodedPath = encodedPath.let { startingPath -> + path("{{path}}") + return@let startingPath + encodedPath.substring(1) + } + {{#hasQueryParams}} + @Suppress("UNNECESSARY_SAFE_CALL") + with(parameters) { + {{#queryParams}} + {{#isContainer}} + {{paramName}}?.let { appendAll("{{baseName}}", it.map { it.toString() }) } + {{/isContainer}} + {{^isContainer}} + {{paramName}}?.let { append("{{baseName}}", it.toString()) } + {{/isContainer}} + {{/queryParams}} + } + {{/hasQueryParams}} + } + body = MultiPartFormDataContent( + formData { + {{#formParams}} + {{#isBinary}} + {{paramName}}?.let { {{paramName}} -> + appendInput( + key = "{{baseName}}", + block = { {{paramName}} }, + headers = Headers.build { + append( + HttpHeaders.ContentType, + ContentType.Application.OctetStream.toString() + ) + append( + HttpHeaders.ContentDisposition, ContentDisposition.File + .withParameter(ContentDisposition.Parameters.FileName, fileName) + .toString() + ) + } + ) + } + {{/isBinary}} + {{^isBinary}} + {{paramName}}?.let { {{paramName}} -> + append( + key = "{{baseName}}", + value = _json.encodeToString( + {{{dataType}}}.serializer(), + {{paramName}} + ) + ) + } + {{/isBinary}} + {{/formParams}} + } + ) + } + } catch (pipeline: ReceivePipelineException) { + throw pipeline.cause + } + {{/hasFormParams}} + } {{/operation}} } -{{/operations}} \ No newline at end of file +{{/operations}} diff --git a/sample/mpp-library/src/formData.yaml b/sample/mpp-library/src/formData.yaml index f787da0..a0d4315 100644 --- a/sample/mpp-library/src/formData.yaml +++ b/sample/mpp-library/src/formData.yaml @@ -41,6 +41,7 @@ paths: properties: signup: $ref: '#/components/schemas/SignupRequest' + required: true avatar: type: string format: binary From 39bc702bd74fe77b92ac2df20071ef26658ac804 Mon Sep 17 00:00:00 2001 From: Sergey Panov Date: Mon, 18 Jul 2022 16:37:17 +0700 Subject: [PATCH 2/5] fixed submitFormWithBinaryData request --- .../src/main/resources/kotlin-ktor-client/api.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-generator/src/main/resources/kotlin-ktor-client/api.mustache b/network-generator/src/main/resources/kotlin-ktor-client/api.mustache index 22b2ac0..0f291cf 100755 --- a/network-generator/src/main/resources/kotlin-ktor-client/api.mustache +++ b/network-generator/src/main/resources/kotlin-ktor-client/api.mustache @@ -147,7 +147,7 @@ import io.ktor.utils.io.core.Input val fileName = "image.jpg" try { - {{#returnType}}return {{/returnType}}_httpClient.submitFormWithBinaryData { + {{#returnType}}return {{/returnType}}_httpClient.submitFormWithBinaryData{{^returnType}}{{/returnType}} { url { takeFrom(_basePath) encodedPath = encodedPath.let { startingPath -> From d6852789c9335f9cc4e3736c7e41d918e9b0ba85 Mon Sep 17 00:00:00 2001 From: Sergey Panov Date: Tue, 19 Jul 2022 17:23:13 +0700 Subject: [PATCH 3/5] tests(not working) --- gradle/libs.versions.toml | 5 ++ sample/mpp-library/build.gradle.kts | 1 + .../src/commonTest/kotlin/FormDataTest.kt | 57 +++++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ef20d5..635dd77 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ robolectricVersion = "4.6.1" ktorClientVersion = "1.6.7" kbignumVersion = "2.4.12" multidexVersion = "2.0.1" +okioVersion = "3.2.0" [libraries] # android @@ -86,3 +87,7 @@ kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serial androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.0.4" } mokoResourcesGradlePlugin = { module = "dev.icerock.moko:resources-generator", version.ref = "mokoResourcesVersion" } mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.1.0" } + + +# other +okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" } \ No newline at end of file diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 89cf8e7..e4f4076 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { commonTestImplementation(libs.kotlinTest) commonTestImplementation(libs.mokoTest) commonTestImplementation(libs.kotlinTestAnnotations) + commonTestImplementation(libs.okio) androidTestImplementation(libs.kotlinTestJUnit) } diff --git a/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt b/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt index 9993c3a..0494e7a 100644 --- a/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt +++ b/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt @@ -5,23 +5,42 @@ import cases.formData.apis.AuthApi import cases.formData.models.Response import cases.formData.models.SignupRequest +import dev.icerock.moko.network.multipart.MultiPartContent import io.ktor.client.engine.mock.* import io.ktor.client.request.forms.* +import io.ktor.http.ContentType +import io.ktor.http.content.MultiPartData +import io.ktor.util.AttributeKey +import io.ktor.util.toCharArray +import io.ktor.utils.io.bits.Memory +import io.ktor.utils.io.bits.storeByteArray +import io.ktor.utils.io.core.AbstractInput +import io.ktor.utils.io.core.ExperimentalIoApi +import io.ktor.utils.io.core.Input +import io.ktor.utils.io.core.internal.ChunkBuffer import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import okio.Buffer +import okio.Source +import kotlin.random.Random import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertTrue class FormDataTest { @Test fun `formData body`() { + val data: ByteArray = mockData() + val source: Source = data.toSource() + val input: Input = source.asInput() + val api = createApi { request -> val body = request.body - assertTrue(body is FormDataContent) + assertTrue(body is MultiPartFormDataContent) // TODO #122 fix test and logic of generated formdata support - assertEquals(expected = "SignupRequest(firstName=first, lastName=last, phone=+799, email=a@b, password=111, passwordRepeat=111, countryId=1, cityId=2, company=test, middleName=null, post=null, interests=null)", actual = body.formData["signup"]) - assertEquals(expected = "teststring", actual = body.formData["avatar"]) + assertContains(body.toByteArray().decodeToString(), "SignupRequest(firstName=first, lastName=last, phone=+799, email=a@b, password=111, passwordRepeat=111, countryId=1, cityId=2, company=test, middleName=null, post=null, interests=null)") + assertContains(body.toByteArray().decodeToString(), data.decodeToString()) respondOk( """ @@ -48,7 +67,7 @@ class FormDataTest { cityId = 2, company = "test" ), - avatar = "teststring" + avatar = input ) } @@ -63,6 +82,13 @@ class FormDataTest { ) } + private fun mockData(): ByteArray { + val random: Random = Random.Default + return ByteArray(8) { random.nextBytes(1)[0] } + } + + private fun ByteArray.toSource(): Source = Buffer().also { it.write(this) } + private fun createApi(mock: MockRequestHandler): AuthApi { val json = Json.Default val httpClient = createMockClient(json, mock) @@ -73,3 +99,26 @@ class FormDataTest { ) } } + +internal class SourceAsInput( + private val source: Source +) : @Suppress("DEPRECATION") AbstractInput(pool = ChunkBuffer.Pool) { + + @OptIn(ExperimentalIoApi::class) + override fun fill(destination: Memory, offset: Int, length: Int): Int { + println("fill $offset $length") + val buffer = Buffer() + source.read(buffer, length.toLong()) + + val chunkByteArray: ByteArray = buffer.readByteArray() + destination.storeByteArray(offset, chunkByteArray) + + return chunkByteArray.size + } + + override fun closeSource() { + source.close() + } +} + +fun Source.asInput(): Input = SourceAsInput(this) From 4872a274b3baba2ba8b934b622a5fde0c7d9412e Mon Sep 17 00:00:00 2001 From: Sergey Panov Date: Tue, 19 Jul 2022 17:53:09 +0700 Subject: [PATCH 4/5] tests(not working) --- sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt b/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt index 0494e7a..1011d19 100644 --- a/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt +++ b/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt @@ -39,8 +39,8 @@ class FormDataTest { val body = request.body assertTrue(body is MultiPartFormDataContent) // TODO #122 fix test and logic of generated formdata support - assertContains(body.toByteArray().decodeToString(), "SignupRequest(firstName=first, lastName=last, phone=+799, email=a@b, password=111, passwordRepeat=111, countryId=1, cityId=2, company=test, middleName=null, post=null, interests=null)") - assertContains(body.toByteArray().decodeToString(), data.decodeToString()) + assertContains(body.toByteArray().decodeToString(), "{\"firstName\":\"first\",\"lastName\":\"last\",\"phone\":\"+799\",\"email\":\"a@b\",\"password\":\"111\",\"passwordRepeat\":\"111\",\"countryId\":1,\"cityId\":2,\"company\":\"test\"}", true) + assertContains(body.toByteArray().decodeToString(), data.decodeToString(), true) respondOk( """ @@ -84,7 +84,7 @@ class FormDataTest { private fun mockData(): ByteArray { val random: Random = Random.Default - return ByteArray(8) { random.nextBytes(1)[0] } + return ByteArray(64) { random.nextBytes(1)[0] } } private fun ByteArray.toSource(): Source = Buffer().also { it.write(this) } From 3a1c853f4b063a43a4023960424161f799c3c335 Mon Sep 17 00:00:00 2001 From: Sergey Panov Date: Wed, 20 Jul 2022 11:51:41 +0700 Subject: [PATCH 5/5] unitTest(not working yet) --- .../src/commonTest/kotlin/FormDataTest.kt | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt b/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt index 1011d19..de5183a 100644 --- a/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt +++ b/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt @@ -18,6 +18,7 @@ import io.ktor.utils.io.core.AbstractInput import io.ktor.utils.io.core.ExperimentalIoApi import io.ktor.utils.io.core.Input import io.ktor.utils.io.core.internal.ChunkBuffer +import io.ktor.utils.io.core.toByteArray import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import okio.Buffer @@ -40,7 +41,7 @@ class FormDataTest { assertTrue(body is MultiPartFormDataContent) // TODO #122 fix test and logic of generated formdata support assertContains(body.toByteArray().decodeToString(), "{\"firstName\":\"first\",\"lastName\":\"last\",\"phone\":\"+799\",\"email\":\"a@b\",\"password\":\"111\",\"passwordRepeat\":\"111\",\"countryId\":1,\"cityId\":2,\"company\":\"test\"}", true) - assertContains(body.toByteArray().decodeToString(), data.decodeToString(), true) + assertContains(body.toByteArray().decodeToString(), data.decodeToString()) respondOk( """ @@ -83,8 +84,29 @@ class FormDataTest { } private fun mockData(): ByteArray { - val random: Random = Random.Default - return ByteArray(64) { random.nextBytes(1)[0] } +// val random: Random = Random.Default +// return ByteArray(64) { random.nextBytes(1)[0] } + val testString: String = "Для женитьбы нужно было согласие отца, и для этого на другой день князь Андрей уехал к отцу.\n" + + "Отец с наружным спокойствием, но внутренней злобой принял сообщение сына. Он не мог понять того, чтобы кто-нибудь хотел изменить жизнь, вносить в нее что-нибудь новое, когда жизнь для него уже кончалась. «Дали бы только дожить так, как я хочу, а потом бы делали, что хотели», — говорил себе старик. С сыном, однако, он употребил ту дипломацию, которую он употреблял в важных случаях. Приняв спокойный тон, он обсудил все дело:\n" + + "Во-первых, женитьба была не блестящая в отношении родства, богатства и знатности. Во-вторых, князь Андрей был не первой молодости и слаб здоровьем (старик особенно налегал на это), а она была очень молода. В-третьих, был сын, которого жалко было отдать девчонке. В-четвертых, наконец, сказал отец, насмешливо глядя на сына, «я тебя прошу, отложи дело на год, съезди за границу, полечись, сыщи, как ты и хочешь, немца для князь Николая, и потом, ежели уж любовь, страсть, упрямство, что хочешь, так велики, тогда женись. И это последнее мое слово, знай, последнее...» — кончил князь таким тоном, которым показывал, что ничто не заставит его изменить свое решение.\n" + + "Князь Андрей ясно видел, что старик надеялся, что чувство его или его будущей невесты не выдержит испытания года или что он сам, старый князь, умрет к этому времени, и решил исполнить волю отца: сделать предложение и отложить свадьбу на год.\n" + + "Через три недели после своего последнего вечера у Ростовых князь Андрей вернулся в Петербург.\n" + + "На другой день после своего объяснения с матерью Наташа ждала целый день Болконского, но он не приехал. На другой, на третий день было то же, Пьер также не приезжал, и Наташа, не зная того, что князь Андрей уехал к отцу, не могла объяснить его отсутствия.\n" + + "Так прошли три недели. Наташа никуда не хотела выезжать, и, как тень, праздная и унылая, ходила по комнатам, вечером тайно от всех плакала и не являлась по вечерам к матери. Она беспрестанно краснела и раздражалась. Ей казалось, что все знают о ее разочаровании, смеются и жалеют о ней. При всей силе внутреннего горя, это тщеславное горе усиливало ее несчастие.\n" + + "Однажды она пришла к графине, хотела что-то сказать ей и вдруг заплакала. Слезы ее были слезы обиженного ребенка, который сам не знает, за что он наказан. Графиня стала успокоивать Наташу. Наташа, вслушивавшаяся сначала в слова матери, вдруг прервала ее:\n" + + "— Перестаньте, мама, я и не думаю и не хочу думать! Так, поездил, и перестал, и перестал...\n" + + "Голос ее задрожал, она чуть не заплакала, но оправилась и спокойно продолжала:\n" + + "— И совсем я не хочу выходить замуж. И я его боюсь; я теперь совсем, совсем успокоилась...\n" + + "На другой день после этого разговора Наташа надела то старое платье, которое было ей особенно известно за доставляемую им по утрам веселость, и с утра начала тот свой прежний образ жизни, от которого она отстала после бала. Она, напившись чаю, пошла в залу, которую она особенно любила за сильный резонанс, и начала петь свои солфеджи (упражнения пения). Окончив первый урок, она остановилась на середине залы и повторила одну музыкальную фразу, особенно понравившуюся ей. Она прислушалась радостно к той (как будто неожиданной для нее) прелести, с которою эти звуки, переливаясь, наполнили всю пустоту залы и медленно замерли, и ей вдруг стало весело. «Что об этом думать много, и так хорошо», — сказала она себе и стала взад и вперед ходить по зале, ступая не простыми шагами по звонкому паркету, но на всяком шагу переступая с каблучка (на ней были новые, любимые башмаки) на носок, и так же радостно, как и к звукам своего голоса, прислушиваясь к этому мерному топоту каблучка и поскрипыванию носка. Проходя мимо зеркала, она заглянула в него. «Вот она я! — как будто говорило выражение ее лица при виде себя. — Ну, и хорошо. И никого мне не нужно».\n" + + "Лакей хотел войти, чтобы убрать что-то в зале, но она не пустила его, опять, затворив за ним дверь, продолжала свою прогулку. Она возвратилась в это утро опять к своему любимому состоянию любви к себе и восхищения перед собою. «Что за прелесть эта Наташа! — сказала она опять про себя словами какого-то третьего, собирательного мужского лица. — Хороша, голос, молода, и никому она не мешает, оставьте только ее в покое». Но сколько бы ни оставляли ее в покое, она уже не могла быть покойна и тотчас же почувствовала это.\n" + + "В передней отворилась дверь подъезда, кто-то спросил: дома ли? — и послышались чьи-то шаги. Наташа смотрелась в зеркало, но она не видала себя. Она слушала звуки в передней. Когда она увидала себя, лицо ее было бледно. Это был он. Она это верно знала, хотя чуть слышала звук его голоса из затворенных дверей.\n" + + "Наташа, бледная и испуганная, вбежала в гостиную.\n" + + "— Мама, Болконский приехал! — сказала она. — Мама, это ужасно, это несносно! Я не хочу... мучиться! Что же мне делать?..\n" + + "Еще графиня не успела ответить ей, как князь Андрей с тревожным и серьезным лицом вошел в гостиную. Как только он увидал Наташу, лицо его просияло. Он поцеловал руку графини и Наташи и сел подле дивана...\n" + + "— Давно уже мы не имели удовольствия... — начала было графиня, но князь Андрей перебил ее, отвечая на ее вопрос и, очевидно, торопясь сказать то, что ему было нужно.\n" + + "— Я не был у вас все это время, потому что был у отца: мне нужно было переговорить с ним о весьма важном деле. Я вчера ночью только вернулся, — сказал он, взглянув на Наташу. — Мне нужно переговорить с вами, графиня, — прибавил он после минутного молчания.\n" + + "Графиня, тяжело вздохнув, опустила глаза.\n" + return testString.toByteArray() } private fun ByteArray.toSource(): Source = Buffer().also { it.write(this) }