diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..13f8d2d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "gradle" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..9680345
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,25 @@
+name: check
+
+on:
+ push:
+ branches:
+ - '*'
+ tags-ignore:
+ - '*'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Configure JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'corretto'
+ java-version: '11'
+ cache: 'gradle'
+ - name: Fetch origin/master
+ run: git fetch origin master
+ - name: Build
+ run: ./gradlew -Psimple.kmm.spotless.ratchet.git.branch=origin/master clean build --no-daemon --no-parallel --stacktrace
diff --git a/.github/workflows/sonatypePublish.yml b/.github/workflows/sonatypePublish.yml
new file mode 100644
index 0000000..9e506c2
--- /dev/null
+++ b/.github/workflows/sonatypePublish.yml
@@ -0,0 +1,60 @@
+name: publish to sonatype
+
+on:
+ push:
+ tags:
+ - 'v*'
+jobs:
+ generate_publish_id:
+ runs-on: ubuntu-latest
+ name: Create staging repository
+ outputs:
+ repository_id: ${{ steps.create.outputs.repository_id }}
+ steps:
+ - id: create
+ uses: nexus-actions/create-nexus-staging-repo@3e5e7209801629febdcf75541a4898710d28df9a
+ with:
+ username: ${{ secrets.PUBLISH_REPOSITORY_USERNAME }}
+ password: ${{ secrets.PUBLISH_REPOSITORY_PASSWORD }}
+ staging_profile_id: ${{ secrets.SONATYPE_PROFILE_ID }}
+ base_url: ${{ secrets.SONATYPE_BASE_URL }}
+ publish:
+ needs: generate_publish_id
+ strategy:
+ matrix:
+ include:
+ - os: windows-latest
+ gradlew: gradlew.bat
+ tasks: publishNativeForWindowsToMavenRepository
+ - os: macos-latest
+ gradlew: gradlew
+ tasks: publishNativeForMacosToMavenRepository
+ - os: ubuntu-latest
+ gradlew: gradlew
+ tasks: publishNativeForLinuxToMavenRepository publishNotNativeToMavenRepository
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Configure JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'corretto'
+ java-version: '11'
+ - name: Fetch project
+ run: git fetch --all -f -P -p
+ - name: Publish
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: >
+ ${{ matrix.tasks }}
+ -Psimple.kmm.spotless.ratchet.git.branch=origin/master
+ -Psimple.kmm.publish.repository.url=${{ secrets.PUBLISH_REPOSITORY_URL }}
+ -Psimple.kmm.publish.repository.id=${{ needs.generate_publish_id.outputs.repository_id }}
+ -Psimple.kmm.publish.username=${{ secrets.PUBLISH_REPOSITORY_USERNAME }}
+ -Psimple.kmm.publish.password=${{ secrets.PUBLISH_REPOSITORY_PASSWORD }}
+ -Psimple.kmm.sign.key.id=${{ secrets.SIGNING_KEY_ID }}
+ -Psimple.kmm.sign.password=${{ secrets.SIGNING_PASSWORD }}
+ -Psimple.kmm.sign.private.key=${{ secrets.SIGNING_PRIVATE_KEY }}
+ -Psimple.kmm.kotlin.compile.target.browser.js.enabled=false
+ --no-daemon --no-parallel --stacktrace
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5f5be73
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+.gradle
+build/
+!**/src/commonMain/**/build/
+!**/src/commonTest/**/build/
+/logging/
+/kotlin-js-store/
+!**/src/**/Env.kt
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/LICENSE_FILE_HEADER b/LICENSE_FILE_HEADER
new file mode 100644
index 0000000..eaf597f
--- /dev/null
+++ b/LICENSE_FILE_HEADER
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) $today.year. Ilia Loginov
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b7bf71e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# Simple kotlix serialization multipart library
+
+[![Maven Central](http://img.shields.io/maven-central/v/io.github.edmondantes/simple-kotlinx-multipart-serialization?color=green&style=flat-square)](https://search.maven.org/search?q=g:io.github.edmondantes%20a:simple-kotlinx-multipart-serialization)
+[![GitHub](http://img.shields.io/github/license/Simple-Kotlin-Project/simple-kotlinx-multipart-serialization?style=flat-square)](https://github.com/Simple-Kotlin-Project/simple-kotlinx-multipart-serialization/blob/master/LICENSE)
+[![Kotlin](https://img.shields.io/badge/kotlin-1.9.10-blue.svg?logo=kotlin)](http://kotlinlang.org)
+[![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/Simple-Kotlin-Project/simple-kotlinx-multipart-serialization/check.yml?branch=master&style=flat-square)](https://github.com/Simple-Kotlin-Project/simple-kotlinx-multipart-serialization/actions/workflows/check.yml)
+
+Small library for serialize and deserialize by multipart content type in http protocol
+
+### How to add library to you project
+
+#### Maven
+
+```xml
+
+
+ io.github.edmondantes
+ simple-kotlinx-multipart-serialization
+ ${simple_library_version}
+
+```
+
+#### Gradle (kotlin)
+
+```kotlin
+implementation("io.github.edmondantes:simple-kotlinx-multipart-serialization:${simple_library_version}")
+```
+
+#### Gradle (groovy)
+
+```groovy
+implementation "io.github.edmondantes:simple-kotlinx-multipart-serialization:${simple_library_version}"
+```
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..b35084d
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,51 @@
+plugins {
+ alias(libs.plugins.simple.kotlin.gradle)
+ alias(libs.plugins.kotlin.serialization.plugin)
+}
+
+group = "io.github.edmondantes"
+
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(libs.simple.kotlin.serialization.utils)
+ implementation(libs.simple.kotlin.multipart)
+ implementation(libs.kotlin.serialization)
+ }
+ }
+
+ val commonTest by getting {
+ dependencies {
+ implementation(kotlin("test"))
+ }
+ }
+ }
+}
+
+licenses {
+ apache2()
+}
+
+developers {
+ developer {
+ name = "Ilia Loginov"
+ email = "masaqaz40@gmail.com"
+ organizationName("github")
+ role("Maintainer")
+ role("Developer")
+ }
+}
+
+simplePom {
+ any {
+ title = "Simple kotlin Telegram API for Kotlin"
+ description = "Library with entities for Telegram API"
+ url = "#github::Simple-Kotlin-Project::${project.name}"
+ scm {
+ url = "#github::Simple-Kotlin-Project::${project.name}::master"
+ connection = "#github::Simple-Kotlin-Project::${project.name}"
+ developerConnection = "#github::Simple-Kotlin-Project::${project.name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..03b4010
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,10 @@
+simple.kmm.kotlin.configuration.enabled=true
+simple.kmm.kotlin.serialization.plugin.enabled=true
+simple.kmm.kotlin.explicit.api.enabled=true
+
+simple.kmm.kotlin.compile.target.jvm.enabled=true
+simple.kmm.kotlin.compile.target.js.enabled=true
+simple.kmm.kotlin.compile.target.browser.js.enabled=false
+simple.kmm.kotlin.compile.target.native.enabled=true
+
+simple.kmm.github.sonatype.publish.enabled=true
\ No newline at end of file
diff --git a/gradle/libs.version.toml b/gradle/libs.version.toml
new file mode 100644
index 0000000..c58c626
--- /dev/null
+++ b/gradle/libs.version.toml
@@ -0,0 +1,15 @@
+[versions]
+simplekmm = "0.6.3"
+simple_kotlin_serialization_utils = "0.8.1"
+kotlin_serialization_version = "1.6.0"
+kotlin_serialization_plugin_version = "1.9.0"
+simple-kotlin-multipart-version = "1.0.0"
+
+[libraries]
+simple_kotlin_serialization_utils = { module = "io.github.edmondantes:simple-kotlinx-serialization-utils", version.ref = "simple_kotlin_serialization_utils" }
+kotlin_serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlin_serialization_version" }
+simple-kotlin-multipart = { module = "io.github.edmondantes:simple-kotlin-multipart", version.ref = "simple-kotlin-multipart-version" }
+
+[plugins]
+simple_kotlin_gradle = { id = "io.github.edmondantes.simple.kmm.gradle.plugin", version.ref = "simplekmm" }
+kotlin_serialization_plugin = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin_serialization_plugin_version" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..249e583
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..06febab
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
\ No newline at end of file
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..1b6c787
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..7a0102c
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,19 @@
+rootProject.name = "simple-kotlinx-multipart-serialization"
+
+dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("./gradle/libs.version.toml"))
+ }
+ }
+}
+
+pluginManagement {
+ repositories {
+ maven {
+ name = "localPluginRepository"
+ url = uri("../local-plugin-repository")
+ }
+ gradlePluginPortal()
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartDynamicHeader.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartDynamicHeader.kt
new file mode 100644
index 0000000..5d92d13
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartDynamicHeader.kt
@@ -0,0 +1,14 @@
+package io.github.edmondantes.multipart.serialization
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialInfo
+
+@OptIn(ExperimentalSerializationApi::class)
+@SerialInfo
+@Target(AnnotationTarget.PROPERTY)
+@Repeatable
+public annotation class MultipartDynamicHeader(
+ val headerName: String,
+ val serialPropertyName: String,
+ val headerAttribute: String = "",
+)
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartDynamicHeaders.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartDynamicHeaders.kt
new file mode 100644
index 0000000..853bb3f
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartDynamicHeaders.kt
@@ -0,0 +1,9 @@
+package io.github.edmondantes.multipart.serialization
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialInfo
+
+@OptIn(ExperimentalSerializationApi::class)
+@SerialInfo
+@Target(AnnotationTarget.PROPERTY)
+public annotation class MultipartDynamicHeaders(val headers: Array)
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartForm.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartForm.kt
new file mode 100644
index 0000000..014c996
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartForm.kt
@@ -0,0 +1,54 @@
+package io.github.edmondantes.multipart.serialization
+
+import io.github.edmondantes.multipart.MultipartFormData
+import io.github.edmondantes.multipart.serialization.config.MultipartFormConfig
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.decoder.MultipartFormRootDecoder
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormRootEncoder
+import io.github.edmondantes.multipart.serialization.util.MultipartFormDataBuilderWithActions
+import kotlinx.serialization.BinaryFormat
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.modules.SerializersModule
+
+/**
+ * Support classes which have properties by only these types:
+ * * Byte
+ * * Char
+ * * Short
+ * * Int
+ * * Long
+ * * Double
+ * * String
+ * * ByteArray
+ * * or List of one of above
+ */
+public class MultipartForm(
+ internal val config: MultipartFormConfig,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : BinaryFormat {
+
+ public val multipartFormEncoderDecoderConfig: MultipartFormEncoderDecoderConfig =
+ MultipartFormEncoderDecoderConfig(config.stringEncoder, config.stringDecoder, serializersModule)
+
+ override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T {
+ return deserializer.deserialize(
+ MultipartFormRootDecoder(
+ MultipartFormData.constructFromMultipart(config.multipartDecoder.decode(config.boundary, bytes)),
+ multipartFormEncoderDecoderConfig,
+ ),
+ )
+ }
+
+ override fun encodeToByteArray(serializer: SerializationStrategy, value: T): ByteArray {
+ val builder = MultipartFormDataBuilderWithActions()
+ val formEncoder = MultipartFormRootEncoder(builder, multipartFormEncoderDecoderConfig)
+ serializer.serialize(formEncoder, value)
+
+ return config.multipartEncoder.encode(config.boundary, builder.build())
+ }
+
+ public companion object {
+ public val Default: MultipartForm = MultipartForm(MultipartFormConfig.Default)
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartStaticHeader.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartStaticHeader.kt
new file mode 100644
index 0000000..9eb422e
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartStaticHeader.kt
@@ -0,0 +1,10 @@
+package io.github.edmondantes.multipart.serialization
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialInfo
+
+@OptIn(ExperimentalSerializationApi::class)
+@SerialInfo
+@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
+@Repeatable
+public annotation class MultipartStaticHeader(val name: String, val value: String, val headerAttribute: String = "")
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartStaticHeaders.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartStaticHeaders.kt
new file mode 100644
index 0000000..48dc72a
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/MultipartStaticHeaders.kt
@@ -0,0 +1,9 @@
+package io.github.edmondantes.multipart.serialization
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialInfo
+
+@OptIn(ExperimentalSerializationApi::class)
+@SerialInfo
+@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
+public annotation class MultipartStaticHeaders(val headers: Array)
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/config/MultipartFormConfig.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/config/MultipartFormConfig.kt
new file mode 100644
index 0000000..fcb4b59
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/config/MultipartFormConfig.kt
@@ -0,0 +1,67 @@
+package io.github.edmondantes.multipart.serialization.config
+
+import io.github.edmondantes.multipart.MultipartDecoder
+import io.github.edmondantes.multipart.MultipartEncoder
+import io.github.edmondantes.multipart.impl.DefaultMultipartEncoderDecoder
+import io.github.edmondantes.multipart.impl.DefaultMultipartEncoderDecoderConfiguration
+import io.github.edmondantes.multipart.serialization.util.nextAlphanumericString
+import kotlinx.serialization.modules.EmptySerializersModule
+import kotlinx.serialization.modules.SerializersModule
+import kotlin.random.Random
+
+public class MultipartFormConfig(
+ public val boundary: String = Random.nextAlphanumericString(70),
+ multipartEncoder: MultipartEncoder? = null,
+ multipartDecoder: MultipartDecoder? = null,
+ public val stringEncoder: (String) -> ByteArray = String::encodeToByteArray,
+ public val stringDecoder: (ByteArray) -> String = ByteArray::decodeToString,
+ public val serializersModule: SerializersModule = EmptySerializersModule(),
+) {
+ public val multipartEncoder: MultipartEncoder
+ public val multipartDecoder: MultipartDecoder
+
+ init {
+ if (multipartEncoder == null || multipartDecoder == null) {
+ val encoderDecoder = DefaultMultipartEncoderDecoder(
+ DefaultMultipartEncoderDecoderConfiguration(
+ shouldCheckBoundary = true,
+ shouldCheckTrailerCounts = true,
+ stringEncoder,
+ stringDecoder,
+ "\n",
+ ),
+ )
+
+ this.multipartEncoder = multipartEncoder ?: encoderDecoder
+ this.multipartDecoder = multipartDecoder ?: encoderDecoder
+ } else {
+ this.multipartEncoder = multipartEncoder
+ this.multipartDecoder = multipartDecoder
+ }
+ }
+
+ public companion object {
+ public val Default: MultipartFormConfig
+
+ init {
+ val encoderDecoder = DefaultMultipartEncoderDecoder(
+ DefaultMultipartEncoderDecoderConfiguration(
+ shouldCheckBoundary = true,
+ shouldCheckTrailerCounts = true,
+ String::encodeToByteArray,
+ ByteArray::decodeToString,
+ "\n",
+ ),
+ )
+
+ Default = MultipartFormConfig(
+ ('1'..'9').joinToString("") + '0' + ('a'..'z').joinToString(""),
+ encoderDecoder,
+ encoderDecoder,
+ String::encodeToByteArray,
+ ByteArray::decodeToString,
+ EmptySerializersModule(),
+ )
+ }
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/config/MultipartFormEncoderDecoderConfig.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/config/MultipartFormEncoderDecoderConfig.kt
new file mode 100644
index 0000000..8fcecb2
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/config/MultipartFormEncoderDecoderConfig.kt
@@ -0,0 +1,9 @@
+package io.github.edmondantes.multipart.serialization.config
+
+import kotlinx.serialization.modules.SerializersModule
+
+public class MultipartFormEncoderDecoderConfig(
+ public val stringEncoder: (String) -> ByteArray,
+ public val stringDecoder: (ByteArray) -> String,
+ public val serializersModule: SerializersModule,
+)
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormCompositeDecoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormCompositeDecoder.kt
new file mode 100644
index 0000000..f646d40
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormCompositeDecoder.kt
@@ -0,0 +1,184 @@
+package io.github.edmondantes.multipart.serialization.decoder
+
+import io.github.edmondantes.multipart.MultipartFormData
+import io.github.edmondantes.multipart.serialization.MultipartDynamicHeader
+import io.github.edmondantes.multipart.serialization.MultipartDynamicHeaders
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.decoder.MultipartFormDecoder.Companion.notSupport
+import io.github.edmondantes.serialization.getElementAllAnnotation
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.serialDescriptor
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormCompositeDecoder(
+ protected open val multipartFormData: MultipartFormData,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : CompositeDecoder {
+
+ protected open var nextElementIndex: Int = 0
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun decodeElementIndex(descriptor: SerialDescriptor): Int =
+ if (nextElementIndex < descriptor.elementsCount) {
+ nextElementIndex++
+ } else {
+ CompositeDecoder.DECODE_DONE
+ }
+
+ override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean =
+ decodeElement(descriptor, index).toBoolean()
+
+ override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte =
+ decodeElement(descriptor, index).toByte()
+
+ override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short =
+ decodeElement(descriptor, index).toShort()
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char =
+ decodeElement(descriptor, index).getOrNull(0)
+ ?: throw SerializationException(
+ "Can not find value for property with name '${descriptor.getElementName(index)}'",
+ )
+
+ override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int =
+ decodeElement(descriptor, index).toInt()
+
+ override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long =
+ decodeElement(descriptor, index).toLong()
+
+ override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float =
+ decodeElement(descriptor, index).toFloat()
+
+ override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double =
+ decodeElement(descriptor, index).toDouble()
+
+ override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String =
+ decodeElement(descriptor, index)
+
+ override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder {
+ notSupport()
+ }
+
+ @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
+ @ExperimentalSerializationApi
+ override fun decodeNullableSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ deserializer: DeserializationStrategy,
+ previousValue: T?,
+ ): T? {
+ val elementDescriptor = descriptor.getElementDescriptor(index)
+
+ var dynamicHeaderValue: List? = null
+ var partValue: List? = null
+
+ decodeDynamicHeader(descriptor, index, {
+ dynamicHeaderValue = it
+ }) {
+ partValue = decodePartValue(descriptor, index)
+ }
+
+ return when (elementDescriptor) {
+ BYTE_ARRAY_DESCRIPTOR -> partValue?.get(0) ?: dynamicHeaderValue?.get(0)?.let { config.stringEncoder(it) }
+ STRING_DESCRIPTOR -> dynamicHeaderValue?.get(0) ?: partValue?.get(0)?.let { config.stringDecoder(it) }
+ else -> if (!dynamicHeaderValue.isNullOrEmpty()) {
+ deserializer.deserialize(
+ MultipartFormDecoder(
+ null,
+ dynamicHeaderValue,
+ config,
+ serializersModule,
+ ),
+ )
+ } else if (!partValue.isNullOrEmpty()) {
+ deserializer.deserialize(
+ MultipartFormDecoder(
+ partValue,
+ null,
+ config,
+ serializersModule,
+ ),
+ )
+ } else {
+ null
+ }
+ } as T?
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun decodeSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ deserializer: DeserializationStrategy,
+ previousValue: T?,
+ ): T =
+ decodeNullableSerializableElement(descriptor, index, deserializer, previousValue)
+ ?: throw SerializationException("Can not find value for '${descriptor.getElementName(index)}'")
+
+ override fun endStructure(descriptor: SerialDescriptor) {}
+
+ @OptIn(ExperimentalSerializationApi::class)
+ protected open fun decodeElement(descriptor: SerialDescriptor, index: Int): String =
+ decodeElementOrNull(descriptor, index).firstOrNull()
+ ?: throw SerializationException(
+ "Can not find value for property with name '${descriptor.getElementName(index)}'",
+ )
+
+ protected open fun decodeElementOrNull(descriptor: SerialDescriptor, index: Int): List =
+ decodeDynamicHeader(descriptor, index, { it }) {
+ decodePartValue(descriptor, index).map(config.stringDecoder)
+ }
+
+ private inline fun decodeDynamicHeader(
+ descriptor: SerialDescriptor,
+ index: Int,
+ dynamicHeaderValueConsumer: (List) -> T,
+ notDynamicHeaderAction: () -> T,
+ ): T {
+ val dynamicHeaders =
+ descriptor
+ .getElementAllAnnotation(index)
+ .filterIsInstance()
+ .flatMap { it.headers.toList() } +
+ descriptor
+ .getElementAllAnnotation(index)
+ .filterIsInstance()
+
+ if (dynamicHeaders.isNotEmpty()) {
+ var headerValue: List? = null
+ var i = 0
+ while (headerValue == null && i < dynamicHeaders.size) {
+ val dynamicHeader = dynamicHeaders[i]
+ val header =
+ multipartFormData.namedParts[dynamicHeader.serialPropertyName]?.mapNotNull { it.headers[dynamicHeader.headerName] }
+ headerValue =
+ if (dynamicHeader.headerAttribute.isNotEmpty()) {
+ header?.mapNotNull { it.attributes[dynamicHeader.headerAttribute] }
+ } else {
+ header?.mapNotNull { it.value }
+ }
+ i++
+ }
+
+ return dynamicHeaderValueConsumer(headerValue ?: emptyList())
+ }
+
+ return notDynamicHeaderAction()
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ protected open fun decodePartValue(descriptor: SerialDescriptor, index: Int): List =
+ multipartFormData.namedParts[descriptor.getElementName(index)]?.map { it.body } ?: emptyList()
+
+ internal companion object {
+ internal val BYTE_ARRAY_DESCRIPTOR: SerialDescriptor = serialDescriptor()
+ internal val STRING_DESCRIPTOR: SerialDescriptor = serialDescriptor()
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormDecoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormDecoder.kt
new file mode 100644
index 0000000..9bba2b8
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormDecoder.kt
@@ -0,0 +1,111 @@
+package io.github.edmondantes.multipart.serialization.decoder
+
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.serialization.decoding.UniqueDecoder
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
+import kotlinx.serialization.descriptors.elementNames
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormDecoder(
+ protected open val bytesValue: List?,
+ protected open val stringValue: List?,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ override val serializersModule: SerializersModule = config.serializersModule
+) : UniqueDecoder {
+ override val id: String = ID
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
+ if (descriptor.kind is StructureKind.LIST) {
+ return MultipartFormListCompositeDecoder(bytesValue, stringValue, config)
+ }
+ throw SerializationException("Can not decode not-flat objects")
+ }
+
+ override fun decodeBoolean(): Boolean =
+ decodeOneStringElement().toBoolean()
+
+ override fun decodeByte(): Byte =
+ decodeOneStringElement().toByte()
+
+ override fun decodeShort(): Short =
+ decodeOneStringElement().toShort()
+
+ override fun decodeChar(): Char =
+ decodeOneStringElement()[0]
+
+ override fun decodeInt(): Int =
+ decodeOneStringElement().toInt()
+
+ override fun decodeLong(): Long =
+ decodeOneStringElement().toLong()
+
+ override fun decodeFloat(): Float =
+ decodeOneStringElement().toFloat()
+
+ override fun decodeDouble(): Double =
+ decodeOneStringElement().toDouble()
+
+ override fun decodeString(): String =
+ decodeOneStringElement()
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
+ val decodedElementName = decodeOneStringElement()
+ val realElementName = enumDescriptor.elementNames.find {
+ it.lowercase() == decodedElementName
+ }
+ ?: throw SerializationException("Can not find element with name '$decodedElementName' in enum '${enumDescriptor.serialName}'")
+
+ return enumDescriptor.getElementIndex(realElementName)
+ }
+
+ override fun decodeInline(descriptor: SerialDescriptor): Decoder {
+ throw SerializationException("Not support")
+ }
+
+ @ExperimentalSerializationApi
+ override fun decodeNotNullMark(): Boolean =
+ stringValue?.firstOrNull()?.isNotEmpty() == true || bytesValue?.firstOrNull()?.isNotEmpty() == true
+
+ @ExperimentalSerializationApi
+ override fun decodeNull(): Nothing? = null
+
+ protected open fun decodeOneStringElement(): String {
+ if (stringValue != null) {
+ return stringValue?.firstOrNull() ?: throw SerializationException("Can not decode value")
+ }
+
+ if (bytesValue != null) {
+ return config.stringDecoder(
+ bytesValue?.firstOrNull() ?: throw SerializationException("Can not decode value"),
+ )
+ }
+
+ throw SerializationException("Can not decode value")
+ }
+
+ protected open fun decodeOneBytesElement(): ByteArray {
+ if (stringValue != null) {
+ config.stringEncoder(stringValue?.firstOrNull() ?: throw SerializationException("Can not decode value"))
+ }
+
+ if (bytesValue != null) {
+ return bytesValue?.firstOrNull() ?: throw SerializationException("Can not decode value")
+ }
+
+ throw SerializationException("Can not decode value")
+ }
+
+ public companion object {
+ internal const val ID: String = "io.github.edmondantes.serialization.multipart.decoder"
+
+ public fun notSupport(): Nothing =
+ throw SerializationException("Can not decode value. Multipart form-data decoder doesn't support to decode this values")
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormListCompositeDecoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormListCompositeDecoder.kt
new file mode 100644
index 0000000..d8387da
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormListCompositeDecoder.kt
@@ -0,0 +1,137 @@
+package io.github.edmondantes.multipart.serialization.decoder
+
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.decoder.MultipartFormCompositeDecoder.Companion.BYTE_ARRAY_DESCRIPTOR
+import io.github.edmondantes.multipart.serialization.decoder.MultipartFormDecoder.Companion.notSupport
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormListCompositeDecoder(
+ protected open val bytesValue: List?,
+ protected open val stringValue: List?,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : CompositeDecoder {
+
+ protected open var nextIndex: Int = 0
+
+ override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
+ return if (nextIndex < (bytesValue?.size ?: stringValue?.size ?: 0)) {
+ nextIndex++
+ } else {
+ CompositeDecoder.DECODE_DONE
+ }
+ }
+
+ override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean =
+ decodeOneStringElement(index).toBoolean()
+
+ override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte =
+ decodeOneStringElement(index).toByte()
+
+ override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char =
+ decodeOneStringElement(index).let {
+ if (it.isEmpty()) {
+ notSupport()
+ }
+
+ it[0]
+ }
+
+ override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short =
+ decodeOneStringElement(index).toShort()
+
+ override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int =
+ decodeOneStringElement(index).toInt()
+
+ override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long =
+ decodeOneStringElement(index).toLong()
+
+ override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float =
+ decodeOneStringElement(index).toFloat()
+
+ override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double =
+ decodeOneStringElement(index).toDouble()
+
+ override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String =
+ decodeOneStringElement(index)
+
+ override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder {
+ notSupport()
+ }
+
+ @ExperimentalSerializationApi
+ @Suppress("UNCHECKED_CAST")
+ override fun decodeNullableSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ deserializer: DeserializationStrategy,
+ previousValue: T?,
+ ): T? {
+ val elementDescriptor = descriptor.getElementDescriptor(index)
+
+ if (BYTE_ARRAY_DESCRIPTOR == elementDescriptor) {
+ return decodeOneBytesElement(index) as T?
+ }
+
+ if (elementDescriptor.kind is PrimitiveKind) {
+ return when (elementDescriptor.kind) {
+ PrimitiveKind.BOOLEAN -> decodeBooleanElement(descriptor, index)
+ PrimitiveKind.BYTE -> decodeByteElement(descriptor, index)
+ PrimitiveKind.CHAR -> decodeCharElement(descriptor, index)
+ PrimitiveKind.SHORT -> decodeShortElement(descriptor, index)
+ PrimitiveKind.INT -> decodeIntElement(descriptor, index)
+ PrimitiveKind.LONG -> decodeLongElement(descriptor, index)
+ PrimitiveKind.DOUBLE -> decodeDoubleElement(descriptor, index)
+ PrimitiveKind.STRING -> decodeStringElement(descriptor, index)
+ else -> null
+ } as T?
+ }
+
+ notSupport()
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun decodeSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ deserializer: DeserializationStrategy,
+ previousValue: T?,
+ ): T =
+ decodeNullableSerializableElement(descriptor, index, deserializer, previousValue)
+ ?: throw SerializationException("Can not decode")
+
+ override fun endStructure(descriptor: SerialDescriptor) {}
+
+ protected open fun decodeOneStringElement(index: Int): String {
+ if (stringValue != null) {
+ return stringValue?.getOrNull(index) ?: throw SerializationException("Can not decode element")
+ }
+
+ if (bytesValue != null) {
+ return bytesValue?.getOrNull(index)?.let(config.stringDecoder)
+ ?: throw SerializationException("Can not decode element")
+ }
+
+ throw SerializationException("Can not decode element")
+ }
+
+ protected open fun decodeOneBytesElement(index: Int): ByteArray {
+ if (stringValue != null) {
+ stringValue?.getOrNull(index)?.let(config.stringEncoder)
+ ?: throw SerializationException("Can not decode element")
+ }
+
+ if (bytesValue != null) {
+ return bytesValue?.getOrNull(index) ?: throw SerializationException("Can not decode element")
+ }
+
+ throw SerializationException("Can not decode element")
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormRootDecoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormRootDecoder.kt
new file mode 100644
index 0000000..6b317de
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/decoder/MultipartFormRootDecoder.kt
@@ -0,0 +1,85 @@
+package io.github.edmondantes.multipart.serialization.decoder
+
+import io.github.edmondantes.multipart.MultipartFormData
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.decoder.MultipartFormDecoder.Companion.ID
+import io.github.edmondantes.multipart.serialization.decoder.MultipartFormDecoder.Companion.notSupport
+import io.github.edmondantes.serialization.decoding.UniqueDecoder
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormRootDecoder(
+ protected open val multipart: MultipartFormData,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : UniqueDecoder {
+ override val id: String = ID
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
+ if (descriptor.kind is StructureKind.CLASS || descriptor.kind is StructureKind.OBJECT) {
+ return MultipartFormCompositeDecoder(multipart, config, serializersModule)
+ } else {
+ throw SerializationException("Multipart form-data decoder can not decode structures with kind '${descriptor.kind}'")
+ }
+ }
+
+ override fun decodeBoolean(): Boolean {
+ notSupport()
+ }
+
+ override fun decodeByte(): Byte {
+ notSupport()
+ }
+
+ override fun decodeChar(): Char {
+ notSupport()
+ }
+
+ override fun decodeShort(): Short {
+ notSupport()
+ }
+
+ override fun decodeInt(): Int {
+ notSupport()
+ }
+
+ override fun decodeLong(): Long {
+ notSupport()
+ }
+
+ override fun decodeFloat(): Float {
+ notSupport()
+ }
+
+ override fun decodeDouble(): Double {
+ notSupport()
+ }
+
+ override fun decodeString(): String {
+ notSupport()
+ }
+
+ override fun decodeInline(descriptor: SerialDescriptor): Decoder {
+ notSupport()
+ }
+
+ override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
+ notSupport()
+ }
+
+ @ExperimentalSerializationApi
+ override fun decodeNotNullMark(): Boolean {
+ notSupport()
+ }
+
+ @ExperimentalSerializationApi
+ override fun decodeNull(): Nothing? {
+ notSupport()
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormCompositeEncoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormCompositeEncoder.kt
new file mode 100644
index 0000000..c9e912b
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormCompositeEncoder.kt
@@ -0,0 +1,187 @@
+package io.github.edmondantes.multipart.serialization.encoder
+
+import io.github.edmondantes.multipart.builder.MultipartPartBuilder
+import io.github.edmondantes.multipart.builder.multipartPart
+import io.github.edmondantes.multipart.builder.multipartPartBuilder
+import io.github.edmondantes.multipart.serialization.MultipartDynamicHeader
+import io.github.edmondantes.multipart.serialization.MultipartDynamicHeaders
+import io.github.edmondantes.multipart.serialization.MultipartStaticHeader
+import io.github.edmondantes.multipart.serialization.MultipartStaticHeaders
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormEncoder.Companion.ID
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormEncoder.Companion.notSupport
+import io.github.edmondantes.multipart.serialization.util.MultipartFormDataBuilderWithActions
+import io.github.edmondantes.serialization.encoding.UniqueCompositeEncoder
+import io.github.edmondantes.serialization.getElementAllAnnotation
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormCompositeEncoder(
+ protected open val multipartBuilder: MultipartFormDataBuilderWithActions,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ protected open val name: String? = null,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : UniqueCompositeEncoder {
+ override val id: String = ID
+
+ override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) {
+ encodeElement(descriptor, index, CharArray(1) { value }.concatToString())
+ }
+
+ override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) {
+ encodeElement(descriptor, index, value.toString())
+ }
+
+ override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder {
+ notSupport()
+ }
+
+ override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) {
+ encodeElement(descriptor, index, value)
+ }
+
+ @ExperimentalSerializationApi
+ override fun encodeNullableSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ serializer: SerializationStrategy,
+ value: T?,
+ ) {
+ if (value != null) {
+ encodeSerializableElement(descriptor, index, serializer, value)
+ }
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun encodeSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ serializer: SerializationStrategy,
+ value: T,
+ ) {
+ when (value) {
+ is String -> encodeElement(descriptor, index, value)
+ is ByteArray -> encodeElement(descriptor, index, value)
+
+ else -> {
+ val part = multipartPartBuilder()
+ serializer.serialize(
+ MultipartFormEncoder(
+ multipartBuilder,
+ config,
+ part,
+ name ?: descriptor.getElementName(index),
+ ),
+ value,
+ )
+
+ part.body?.also { encodeElement(descriptor, index, it) }
+ }
+ }
+ }
+
+ override fun endStructure(descriptor: SerialDescriptor) {}
+
+ protected open fun encodeElement(descriptor: SerialDescriptor, index: Int, value: String) {
+ if (!tryToEncodeDynamicHeader(descriptor, index, value)) {
+ encodeElement(descriptor, index, config.stringEncoder(value), false)
+ }
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ protected open fun encodeElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ value: ByteArray,
+ tryEncodeDynamicHeader: Boolean = true,
+ ) {
+ if (tryEncodeDynamicHeader) {
+ tryToEncodeDynamicHeader(descriptor, index, value.decodeToString())
+ }
+
+ multipartBuilder.add(
+ name ?: descriptor.getElementName(index),
+ multipartPart {
+ body = value
+
+ val staticHeaders =
+ descriptor
+ .getElementAllAnnotation(index)
+ .filterIsInstance()
+ .flatMap { it.headers.toList() } +
+ descriptor
+ .getElementAllAnnotation(index)
+ .filterIsInstance()
+
+ staticHeaders.forEach {
+ encodeHeaderValueOrAttribute(it.name, it.headerAttribute, this, it.value)
+ }
+ },
+ )
+ }
+
+ protected open fun tryToEncodeDynamicHeader(descriptor: SerialDescriptor, index: Int, value: String): Boolean {
+ val dynamicHeader = getDynamicHeaders(descriptor, index)
+
+ dynamicHeader.forEach { header ->
+ multipartBuilder.addOnBuild { formDataBuilder ->
+ formDataBuilder.edit(header.serialPropertyName) { partBuilder ->
+ encodeHeaderValueOrAttribute(header.headerName, header.headerAttribute, partBuilder, value)
+ }
+ }
+ }
+
+ return dynamicHeader.isNotEmpty()
+ }
+
+ protected open fun getDynamicHeaders(descriptor: SerialDescriptor, index: Int): List =
+ descriptor
+ .getElementAllAnnotation(index)
+ .filterIsInstance()
+ .flatMap { it.headers.toList() } +
+ descriptor
+ .getElementAllAnnotation(index)
+ .filterIsInstance()
+
+ protected open fun encodeHeaderValueOrAttribute(
+ name: String,
+ headerAttribute: String,
+ builder: MultipartPartBuilder,
+ value: String,
+ ) {
+ builder.header(name) {
+ if (headerAttribute.isNotBlank()) {
+ attributes[headerAttribute] = value
+ } else {
+ this.value = value
+ }
+ }
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormEncoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormEncoder.kt
new file mode 100644
index 0000000..3f54ede
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormEncoder.kt
@@ -0,0 +1,105 @@
+package io.github.edmondantes.multipart.serialization.encoder
+
+import io.github.edmondantes.multipart.builder.MultipartPartBuilder
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.util.MultipartFormDataBuilderWithActions
+import io.github.edmondantes.serialization.encoding.UniqueEncoder
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
+import kotlinx.serialization.encoding.CompositeEncoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.modules.SerializersModule
+
+@OptIn(ExperimentalSerializationApi::class)
+public open class MultipartFormEncoder(
+ protected open val builder: MultipartFormDataBuilderWithActions,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ protected open val partBuilder: MultipartPartBuilder? = null,
+ protected open val name: String? = null,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : UniqueEncoder {
+ override val id: String = ID
+
+ override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder =
+ when (descriptor.kind) {
+ is StructureKind.LIST -> {
+ MultipartFormListCompositeEncoder(
+ builder,
+ name
+ ?: throw SerializationException("Multipart form-data encoder can not encode list without name"),
+ config,
+ serializersModule,
+ )
+ }
+
+ is StructureKind.CLASS, is StructureKind.OBJECT -> {
+ throw SerializationException("Multipart form-data encoder can not encode non-flat structures")
+ }
+
+ else -> {
+ throw SerializationException("Multipart form-data encoder can not encode structures with kind '${descriptor.kind}'")
+ }
+ }
+
+ override fun encodeBoolean(value: Boolean) {
+ encodeElement(value)
+ }
+
+ override fun encodeByte(value: Byte) {
+ encodeElement(value)
+ }
+
+ override fun encodeChar(value: Char) {
+ encodeElement(value)
+ }
+
+ override fun encodeShort(value: Short) {
+ encodeElement(value)
+ }
+
+ override fun encodeInt(value: Int) {
+ encodeElement(value)
+ }
+
+ override fun encodeLong(value: Long) {
+ encodeElement(value)
+ }
+
+ override fun encodeFloat(value: Float) {
+ encodeElement(value)
+ }
+
+ override fun encodeDouble(value: Double) {
+ encodeElement(value)
+ }
+
+ override fun encodeString(value: String) {
+ encodeElement(value)
+ }
+
+ override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
+ encodeElement(enumDescriptor.getElementName(index))
+ }
+
+ override fun encodeInline(descriptor: SerialDescriptor): Encoder {
+ notSupport()
+ }
+
+ @ExperimentalSerializationApi
+ override fun encodeNull() {
+ (partBuilder ?: notSupport()).body = ByteArray(0)
+ }
+
+ protected open fun encodeElement(value: Any) {
+ (partBuilder ?: notSupport()).body = config.stringEncoder(value.toString())
+ }
+
+ public companion object {
+ internal const val ID: String = "io.github.edmondantes.serialization.multipart.encoder"
+
+ public fun notSupport(): Nothing =
+ throw SerializationException("Can not encode value. Multipart form-data encoder doesn't support to encode this values")
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormListCompositeEncoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormListCompositeEncoder.kt
new file mode 100644
index 0000000..2eb6ae9
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormListCompositeEncoder.kt
@@ -0,0 +1,106 @@
+package io.github.edmondantes.multipart.serialization.encoder
+
+import io.github.edmondantes.multipart.builder.multipartPart
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormEncoder.Companion.ID
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormEncoder.Companion.notSupport
+import io.github.edmondantes.multipart.serialization.util.MultipartFormDataBuilderWithActions
+import io.github.edmondantes.serialization.encoding.UniqueCompositeEncoder
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormListCompositeEncoder(
+ protected open val builder: MultipartFormDataBuilderWithActions,
+ protected open val name: String,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : UniqueCompositeEncoder {
+ override val id: String = ID
+
+ override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) {
+ encodeElement(value.toString())
+ }
+
+ override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) {
+ encodeElement(value)
+ }
+
+ override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder {
+ notSupport()
+ }
+
+ @ExperimentalSerializationApi
+ override fun encodeNullableSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ serializer: SerializationStrategy,
+ value: T?,
+ ) {
+ if (value != null) {
+ encodeSerializableElement(descriptor, index, serializer, value)
+ }
+ }
+
+ override fun encodeSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ serializer: SerializationStrategy,
+ value: T,
+ ) {
+ when (value) {
+ is ByteArray -> builder.add(name, multipartPart { body = value })
+ is Boolean -> encodeBooleanElement(descriptor, index, value)
+ is Byte -> encodeByteElement(descriptor, index, value)
+ is Char -> encodeCharElement(descriptor, index, value)
+ is Short -> encodeShortElement(descriptor, index, value)
+ is Int -> encodeIntElement(descriptor, index, value)
+ is Long -> encodeLongElement(descriptor, index, value)
+ is Float -> encodeFloatElement(descriptor, index, value)
+ is Double -> encodeDoubleElement(descriptor, index, value)
+ is String -> encodeStringElement(descriptor, index, value)
+ else -> notSupport()
+ }
+ }
+
+ override fun endStructure(descriptor: SerialDescriptor) {}
+
+ protected open fun encodeElement(value: String) {
+ builder.add(
+ name,
+ multipartPart {
+ body = config.stringEncoder(value)
+ },
+ )
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormRootEncoder.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormRootEncoder.kt
new file mode 100644
index 0000000..6f9bcaa
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/encoder/MultipartFormRootEncoder.kt
@@ -0,0 +1,80 @@
+package io.github.edmondantes.multipart.serialization.encoder
+
+import io.github.edmondantes.multipart.serialization.config.MultipartFormEncoderDecoderConfig
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormEncoder.Companion.ID
+import io.github.edmondantes.multipart.serialization.encoder.MultipartFormEncoder.Companion.notSupport
+import io.github.edmondantes.multipart.serialization.util.MultipartFormDataBuilderWithActions
+import io.github.edmondantes.serialization.encoding.UniqueEncoder
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
+import kotlinx.serialization.encoding.CompositeEncoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.modules.SerializersModule
+
+public open class MultipartFormRootEncoder(
+ protected open val builder: MultipartFormDataBuilderWithActions,
+ protected open val config: MultipartFormEncoderDecoderConfig,
+ override val serializersModule: SerializersModule = config.serializersModule,
+) : UniqueEncoder {
+ override val id: String = ID
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
+ if (descriptor.kind is StructureKind.CLASS || descriptor.kind is StructureKind.OBJECT) {
+ return MultipartFormCompositeEncoder(builder, config)
+ } else {
+ throw SerializationException("Multipart form-data encoder can not encode root structures with kind '${descriptor.kind}'")
+ }
+ }
+
+ override fun encodeBoolean(value: Boolean) {
+ notSupport()
+ }
+
+ override fun encodeByte(value: Byte) {
+ notSupport()
+ }
+
+ override fun encodeChar(value: Char) {
+ notSupport()
+ }
+
+ override fun encodeShort(value: Short) {
+ notSupport()
+ }
+
+ override fun encodeInt(value: Int) {
+ notSupport()
+ }
+
+ override fun encodeLong(value: Long) {
+ notSupport()
+ }
+
+ override fun encodeFloat(value: Float) {
+ notSupport()
+ }
+
+ override fun encodeDouble(value: Double) {
+ notSupport()
+ }
+
+ override fun encodeString(value: String) {
+ notSupport()
+ }
+
+ override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
+ notSupport()
+ }
+
+ override fun encodeInline(descriptor: SerialDescriptor): Encoder {
+ notSupport()
+ }
+
+ @ExperimentalSerializationApi
+ override fun encodeNull() {
+ notSupport()
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/util/MultipartFormDataBuilderWithActions.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/util/MultipartFormDataBuilderWithActions.kt
new file mode 100644
index 0000000..9131575
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/util/MultipartFormDataBuilderWithActions.kt
@@ -0,0 +1,21 @@
+package io.github.edmondantes.multipart.serialization.util
+
+import io.github.edmondantes.multipart.MultipartFormData
+import io.github.edmondantes.multipart.builder.MultipartFormDataBuilder
+
+public class MultipartFormDataBuilderWithActions : MultipartFormDataBuilder() {
+
+ private val onBuildActions: MutableList<(MultipartFormDataBuilder) -> Unit> = mutableListOf()
+
+ public fun addOnBuild(action: (MultipartFormDataBuilder) -> Unit): MultipartFormDataBuilderWithActions = apply {
+ onBuildActions.add(action)
+ }
+
+ override fun build(): MultipartFormData {
+ onBuildActions.forEach { action ->
+ action(this)
+ }
+
+ return super.build()
+ }
+}
diff --git a/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/util/RandomUtil.kt b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/util/RandomUtil.kt
new file mode 100644
index 0000000..275e63c
--- /dev/null
+++ b/src/commonMain/kotlin/io/github/edmondantes/multipart/serialization/util/RandomUtil.kt
@@ -0,0 +1,21 @@
+package io.github.edmondantes.multipart.serialization.util
+
+import kotlin.random.Random
+
+public fun Random.nextChar(from: Char, until: Char): Char =
+ nextInt(from.code, until.code).toChar()
+
+public fun Random.nextAlphanumericString(size: Int): String {
+ val array = CharArray(size)
+ for (i in array.indices) {
+ array[i] =
+ when (Random.nextInt(0, 3)) {
+ 0 -> nextChar('a', 'z')
+ 1 -> nextChar('A', 'Z')
+ 2 -> nextChar('0', '9')
+ else -> error("Can not generate character")
+ }
+ }
+
+ return array.concatToString()
+}
diff --git a/src/commonTest/kotlin/io/github/edmondantes/serialization/multipart/encoder/MultipartFormDataEncoderTest.kt b/src/commonTest/kotlin/io/github/edmondantes/serialization/multipart/encoder/MultipartFormDataEncoderTest.kt
new file mode 100644
index 0000000..f12240d
--- /dev/null
+++ b/src/commonTest/kotlin/io/github/edmondantes/serialization/multipart/encoder/MultipartFormDataEncoderTest.kt
@@ -0,0 +1,503 @@
+package io.github.edmondantes.serialization.multipart.encoder
+
+import io.github.edmondantes.multipart.Multipart
+import io.github.edmondantes.multipart.MultipartPart
+import io.github.edmondantes.multipart.builder.MultipartFormDataBuilder
+import io.github.edmondantes.multipart.builder.MultipartPartBuilder
+import io.github.edmondantes.multipart.builder.multipartFormData
+import io.github.edmondantes.multipart.builder.multipartPart
+import io.github.edmondantes.multipart.serialization.MultipartDynamicHeader
+import io.github.edmondantes.multipart.serialization.MultipartForm
+import io.github.edmondantes.multipart.serialization.MultipartStaticHeader
+import io.github.edmondantes.multipart.serialization.util.nextAlphanumericString
+import io.github.edmondantes.multipart.serialization.util.nextChar
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.decodeFromByteArray
+import kotlinx.serialization.encodeToByteArray
+import kotlin.random.Random
+import kotlin.reflect.KProperty0
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class MultipartFormDataEncoderTest {
+
+ @Test
+ fun encodeEmptyTest() {
+ encode(::EmptyTestClass) { Multipart.Empty }
+ }
+
+ @Test
+ fun decodeEmptyTest() {
+ decode(::EmptyTestClass) { Multipart.Empty }
+ }
+
+ @Test
+ fun encodeNullTest() {
+ encode(::NullTestClass, ::NULL_TEST_BYTES)
+ }
+
+ @Test
+ fun decodeNullTest() {
+ decode(::NullTestClass, ::NULL_TEST_BYTES)
+ }
+
+ @Test
+ fun encodeFlatTest() {
+ encode(::FlatTestClass, ::FLAT_TEST_BYTES)
+ }
+
+ @Test
+ fun decodeFlatTest() {
+ decode(::FlatTestClass, ::FLAT_TEST_BYTES)
+ }
+
+ /**
+ * Because default string encoder is [String.encodeToByteArray], and the method can change some characters
+ *
+ * **See also** [Kotlin Docs](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/encode-to-byte-array.html)
+ */
+ @Test
+ fun encodeWrongChar() {
+ val obj = FlatTestClass(char = Char(56152))
+ encode({ obj }, ::FLAT_TEST_BYTES)
+ }
+
+ /**
+ * Because default string encoder is [String.encodeToByteArray], and the method can change some characters
+ *
+ * **See also** [Kotlin Docs](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/encode-to-byte-array.html)
+ */
+ @Test
+ fun decodeWrongChar() {
+ assertFailsWith {
+ val obj = FlatTestClass(char = Char(56152))
+ decode({ obj }, ::FLAT_TEST_BYTES)
+ }
+ }
+
+ @Test
+ fun encodeByteArrayTest() {
+ encode(::ByteArrayTestClass, ::BYTE_ARRAY_BYTES)
+ }
+
+ @Test
+ fun decodeByteArrayTest() {
+ decode(::ByteArrayTestClass, ::BYTE_ARRAY_BYTES)
+ }
+
+ @Test
+ fun encodeIntArrayTest() {
+ encode(::IntArrayTestClass, ::INT_ARRAY_BYTES)
+ }
+
+ @Test
+ fun decodeIntArrayTest() {
+ decode(::IntArrayTestClass, ::INT_ARRAY_BYTES)
+ }
+
+ @Test
+ fun encodeListTest() {
+ encode(::ListTestClass, ::LIST_BYTES)
+ }
+
+ @Test
+ fun decodeListTest() {
+ decode(::ListTestClass, ::LIST_BYTES)
+ }
+
+ @Test
+ fun encodeListByteArrayTest() {
+ encode(::ListByteArrayTestClass, ::LIST_BYTE_ARRAY_BYTES)
+ }
+
+ @Test
+ fun decodeListByteArrayTest() {
+ decode(::ListByteArrayTestClass, ::LIST_BYTE_ARRAY_BYTES)
+ }
+
+ @Test
+ fun encodeListIntArrayTest() {
+ assertFailsWith("Can not encode value. Multipart form-data encoder doesn't support to encode this values") {
+ encode(::ListIntArrayTestClass, ::LIST_INT_ARRAY_BYTES)
+ }
+ }
+
+ @Test
+ fun decodeListIntArrayTest() {
+ assertFailsWith("Can not decode value. Multipart form-data decoder doesn't support to decode this values") {
+ decode(::ListIntArrayTestClass, ::LIST_INT_ARRAY_BYTES)
+ }
+ }
+
+ @Test
+ fun encodeListListTest() {
+ assertFailsWith("Can not encode value. Multipart form-data encoder doesn't support to encode this values") {
+ encode(::ListListTestClass, ::LIST_LIST_BYTES)
+ }
+ }
+
+ @Test
+ fun decodeListListTest() {
+ assertFailsWith("Can not encode value. Multipart form-data encoder doesn't support to encode this values") {
+ decode(::ListListTestClass, ::LIST_LIST_BYTES)
+ }
+ }
+
+ @Test
+ fun encodeMapTest() {
+ assertFailsWith("Can not encode value. Multipart form-data encoder doesn't support to encode this values") {
+ encode(::MapTestClass) { Multipart.Empty }
+ }
+ }
+
+ @Test
+ fun encodeStaticHeaderTest() {
+ encode(::StaticHeaderTestClass, ::STATIC_HEADER_BYTES)
+ }
+
+ @Test
+ fun decodeStaticHeaderTest() {
+ decode(::StaticHeaderTestClass, ::STATIC_HEADER_BYTES)
+ }
+
+ @Test
+ fun encodeStaticHeaderAttributeTest() {
+ encode(::StaticHeaderAttributeTestClass, ::STATIC_HEADER_ATTRIBUTE_BYTES)
+ }
+
+ @Test
+ fun decodeStaticHeaderAttributeTest() {
+ decode(::StaticHeaderAttributeTestClass, ::STATIC_HEADER_ATTRIBUTE_BYTES)
+ }
+
+ @Test
+ fun encodeDynamicHeaderTest() {
+ encode(::DynamicHeaderTestClass, ::DYNAMIC_HEADER_BYTES)
+ }
+
+ @Test
+ fun decodeDynamicHeaderTest() {
+ decode(::DynamicHeaderTestClass, ::DYNAMIC_HEADER_BYTES)
+ }
+
+ @Test
+ fun encodeDynamicHeaderAttributeTest() {
+ encode(::DynamicHeaderAttributeTestClass, ::DYNAMIC_HEADER_ATTRIBUTE_BYTES)
+ }
+
+ @Test
+ fun decodeDynamicHeaderAttributeTest() {
+ decode(::DynamicHeaderAttributeTestClass, ::DYNAMIC_HEADER_ATTRIBUTE_BYTES)
+ }
+
+ private inline fun encode(factory: () -> T, multipartFactory: (T) -> Multipart) {
+ val objForEncode = factory()
+ val expected = multipartFactory(objForEncode).let { encoder.encode(boundary, it) }
+ val actual = format.encodeToByteArray(objForEncode)
+
+ assertContentEquals(expected, actual)
+ }
+
+ private inline fun decode(factory: () -> T, multipartFactory: (T) -> Multipart) {
+ val expected = factory()
+ val objForDecode = multipartFactory(expected).let { encoder.encode(boundary, it) }
+ val actual = format.decodeFromByteArray(objForDecode)
+
+ assertEquals(expected, actual)
+ }
+
+ private companion object {
+ val format = MultipartForm.Default
+ val encoder = format.config.multipartEncoder
+ val boundary = format.config.boundary
+
+ fun NULL_TEST_BYTES(obj: NullTestClass): Multipart = multipartFormData {
+ addNamed(obj::simple)
+ }
+
+ fun FLAT_TEST_BYTES(obj: FlatTestClass): Multipart = multipartFormData {
+ addNamed(obj::byte)
+ addNamed(obj::short)
+ addNamed(obj::char)
+ addNamed(obj::int)
+ addNamed(obj::long)
+ addNamed(obj::float)
+ addNamed(obj::double)
+ addNamed(obj::string)
+ }
+
+ fun BYTE_ARRAY_BYTES(obj: ByteArrayTestClass): Multipart = multipartFormData {
+ addNamed(obj::simple)
+ addNamed(obj::array)
+ }
+
+ fun INT_ARRAY_BYTES(obj: IntArrayTestClass): Multipart = multipartFormData {
+ addNamed(obj::simple)
+ obj.array.forEach { element ->
+ add(
+ "array",
+ multipartPart {
+ body = element.toString().encodeToByteArray()
+ },
+ )
+ }
+ }
+
+ fun LIST_BYTES(obj: ListTestClass): Multipart = multipartFormData {
+ obj.list.forEach { element ->
+ add(
+ "list",
+ multipartPart {
+ body = element.encodeToByteArray()
+ },
+ )
+ }
+ }
+
+ fun LIST_BYTE_ARRAY_BYTES(obj: ListByteArrayTestClass): Multipart = multipartFormData {
+ obj.list.forEach { element ->
+ add(
+ "list",
+ multipartPart {
+ body = element
+ },
+ )
+ }
+ }
+
+ fun LIST_INT_ARRAY_BYTES(obj: ListIntArrayTestClass): Multipart = multipartFormData {
+ obj.list.flatMap { it.toList() }.forEach { element ->
+ add(
+ "list",
+ multipartPart {
+ body = element.toString().encodeToByteArray()
+ },
+ )
+ }
+ }
+
+ fun LIST_LIST_BYTES(obj: ListListTestClass): Multipart = multipartFormData {
+ obj.list.flatten().forEach { element ->
+ add("list", multipartPart { body = element.encodeToByteArray() })
+ }
+ }
+
+ fun STATIC_HEADER_BYTES(obj: StaticHeaderTestClass): Multipart = multipartFormData {
+ addNamed(obj::field) {
+ header("Header") {
+ value = "Element1"
+ }
+ }
+ }
+
+ fun STATIC_HEADER_ATTRIBUTE_BYTES(obj: StaticHeaderAttributeTestClass): Multipart = multipartFormData {
+ addNamed(obj::field) {
+ header("Header") {
+ value = "Element1"
+ attributes["attr"] = "Attribute1"
+ }
+ }
+ }
+
+ fun DYNAMIC_HEADER_BYTES(obj: DynamicHeaderTestClass): Multipart = multipartFormData {
+ addNamed(obj::field) {
+ header("Header") {
+ value = obj.header
+ }
+ }
+ }
+
+ fun DYNAMIC_HEADER_ATTRIBUTE_BYTES(obj: DynamicHeaderAttributeTestClass): Multipart = multipartFormData {
+ addNamed(obj::field) {
+ header("Header") {
+ value = obj.header
+ attributes["attr"] = obj.attribute
+ }
+ }
+ }
+
+ @Serializable
+ class EmptyTestClass {
+ override fun equals(other: Any?): Boolean =
+ if (this === other || other is EmptyTestClass) {
+ true
+ } else {
+ super.equals(other)
+ }
+
+ override fun hashCode(): Int {
+ return this::class.hashCode()
+ }
+ }
+
+ @Serializable
+ data class NullTestClass(
+ val simple: String = Random.nextAlphanumericString(10),
+ val nullable: String? = null,
+ )
+
+ @Serializable
+ data class FlatTestClass(
+ val byte: Byte = Random.nextBytes(1)[0],
+ val short: Short = Random.nextInt(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort(),
+ val char: Char = Random.nextChar('a', 'z'),
+ val int: Int = Random.nextInt(),
+ val long: Long = Random.nextLong(),
+ val float: Float = Random.nextFloat(),
+ val double: Double = Random.nextDouble(),
+ val string: String = Random.nextAlphanumericString(100),
+ )
+
+ @Serializable
+ data class ByteArrayTestClass(
+ val simple: String = Random.nextAlphanumericString(10),
+ val array: ByteArray = Random.nextAlphanumericString(10).encodeToByteArray(),
+ ) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ByteArrayTestClass) return false
+
+ if (simple != other.simple) return false
+ if (!array.contentEquals(other.array)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = simple.hashCode()
+ result = 31 * result + array.contentHashCode()
+ return result
+ }
+ }
+
+ @Serializable
+ data class IntArrayTestClass(
+ val simple: String = Random.nextAlphanumericString(10),
+ val array: IntArray = generateSequence { Random.nextInt() }.take(10).toList().toIntArray(),
+ ) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is IntArrayTestClass) return false
+
+ if (simple != other.simple) return false
+ if (!array.contentEquals(other.array)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = simple.hashCode()
+ result = 31 * result + array.contentHashCode()
+ return result
+ }
+ }
+
+ @Serializable
+ data class ListTestClass(
+ val list: List = generateSequence { Random.nextAlphanumericString(10) }.take(10).toList(),
+ )
+
+ @Serializable
+ data class ListByteArrayTestClass(
+ val list: List = generateSequence { Random.nextBytes(70) }.take(10).toList(),
+ ) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ListByteArrayTestClass) return false
+
+ if (list.size != other.list.size) return false
+
+ list.forEachIndexed { index, element ->
+ if (!element.contentEquals(other.list[index])) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return list.hashCode()
+ }
+ }
+
+ @Serializable
+ data class ListIntArrayTestClass(
+ val list: List = generateSequence {
+ generateSequence { Random.nextInt() }.take(10).toList().toIntArray()
+ }.take(10).toList(),
+ )
+
+ @Serializable
+ data class ListListTestClass(
+ val list: List> = generateSequence {
+ generateSequence { Random.nextAlphanumericString(10) }.take(10).toList()
+ }.take(10).toList(),
+ )
+
+ @Serializable
+ data class MapTestClass(
+ val map: Map = generateSequence { Random.nextAlphanumericString(10) }
+ .take(10)
+ .map { it to it }
+ .toMap(),
+ )
+
+ @Serializable
+ data class StaticHeaderTestClass(
+ @MultipartStaticHeader("Header", "Element1")
+ val field: String = Random.nextAlphanumericString(10),
+ )
+
+ @Serializable
+ data class StaticHeaderAttributeTestClass(
+ @MultipartStaticHeader("Header", "Element1")
+ @MultipartStaticHeader("Header", "Attribute1", "attr")
+ val field: String = Random.nextAlphanumericString(10),
+ )
+
+ @Serializable
+ data class DynamicHeaderTestClass(
+ val field: String = Random.nextAlphanumericString(10),
+ @MultipartDynamicHeader("Header", "field")
+ val header: String = Random.nextAlphanumericString(10),
+ )
+
+ @Serializable
+ data class DynamicHeaderAttributeTestClass(
+ val field: String = Random.nextAlphanumericString(10),
+ @MultipartDynamicHeader("Header", "field")
+ val header: String = Random.nextAlphanumericString(10),
+ @MultipartDynamicHeader("Header", "field", "attr")
+ val attribute: String = Random.nextAlphanumericString(10),
+ )
+
+ private inline fun Any.part(block: MultipartPartBuilder.() -> Unit = {}): MultipartPart =
+ multipartPart {
+ body = if (this@part is ByteArray) {
+ this@part
+ } else {
+ this@part.toString().encodeToByteArray()
+ }
+ apply(block)
+ }
+
+ private inline fun MultipartFormDataBuilder.addNamed(
+ name: String,
+ any: Any,
+ block: MultipartPartBuilder.() -> Unit = {},
+ ) {
+ add(name, any.part(block))
+ }
+
+ private inline fun MultipartFormDataBuilder.addNamed(
+ property: KProperty0<*>,
+ block: MultipartPartBuilder.() -> Unit = {},
+ ) {
+ property.get()?.also { addNamed(property.name, it, block) }
+ }
+ }
+}