├── .circleci └── config.yml ├── .github └── workflows │ └── auto-author-assign.yml ├── .gitignore ├── .idea ├── .gitignore ├── KMapper.iml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── flexCompiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinc.xml ├── misc.xml ├── sbt.xml ├── uiDesigner.xml └── vcs.xml ├── LICENSE ├── README.ja.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main └── kotlin │ └── com │ └── mapk │ ├── annotations │ ├── KConverter.kt │ ├── KGetterAlias.kt │ └── KGetterIgnore.kt │ ├── conversion │ └── KConvert.kt │ └── kmapper │ ├── BoundKMapper.kt │ ├── BoundParameterForMap.kt │ ├── DummyConstructor.kt │ ├── KMapper.kt │ ├── ParameterForMap.kt │ ├── ParameterUtils.kt │ ├── PlainKMapper.kt │ └── PlainParameterForMap.kt └── test ├── java └── com │ └── mapk │ └── kmapper │ └── StaticMethodConverter.java └── kotlin └── com └── mapk └── kmapper ├── BoundKMapperInitTest.kt ├── BoundParameterForMapTest.kt ├── ConversionTest.kt ├── ConverterKMapperTest.kt ├── DefaultArgumentTest.kt ├── EnumMappingTest.kt ├── KGetterIgnoreTest.kt ├── KParameterFlattenTest.kt ├── ParameterNameConverterTest.kt ├── PropertyAliasTest.kt ├── RecursiveMappingTest.kt ├── SimpleKMapperTest.kt ├── StringMappingTest.kt └── testcommons └── JvmLanguage.kt /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Java Gradle CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-java/ for more details 4 | # 5 | version: 2.1 6 | orbs: 7 | codecov: codecov/codecov@1.0.5 8 | jobs: 9 | build: 10 | docker: 11 | # specify the version you desire here 12 | - image: circleci/openjdk:8-jdk 13 | 14 | # Specify service dependencies here if necessary 15 | # CircleCI maintains a library of pre-built images 16 | # documented at https://circleci.com/docs/2.0/circleci-images/ 17 | # - image: circleci/postgres:9.4 18 | 19 | working_directory: ~/repo 20 | 21 | environment: 22 | # Customize the JVM maximum heap limit 23 | JVM_OPTS: -Xmx3200m 24 | TERM: dumb 25 | 26 | steps: 27 | - checkout 28 | 29 | # Download and cache dependencies 30 | - restore_cache: 31 | keys: 32 | - v1-dependencies-{{ checksum "build.gradle.kts" }} 33 | # fallback to using the latest cache if no exact match is found 34 | - v1-dependencies- 35 | 36 | - run: gradle dependencies 37 | 38 | - save_cache: 39 | paths: 40 | - ~/.gradle 41 | key: v1-dependencies-{{ checksum "build.gradle.kts" }} 42 | 43 | # run lints! 44 | - run: gradle ktlintCheck 45 | # run tests! 46 | - run: gradle test 47 | # upload coverages to codecov 48 | - run: bash <(curl -s https://codecov.io/bash) 49 | -------------------------------------------------------------------------------- /.github/workflows/auto-author-assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Author Assign' 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | assign-author: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: toshimaru/auto-author-assign@v1.2.0 12 | with: 13 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /build/ 4 | /target/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/KMapper.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/flexCompiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/sbt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2020 wrongwrong(https://github.com/k163377) 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 2 | [![CircleCI](https://circleci.com/gh/ProjectMapK/KMapper.svg?style=svg)](https://circleci.com/gh/ProjectMapK/KMapper) 3 | [![](https://jitci.com/gh/ProjectMapK/KMapper/svg)](https://jitci.com/gh/ProjectMapK/KMapper) 4 | [![codecov](https://codecov.io/gh/ProjectMapK/KMapper/branch/master/graph/badge.svg)](https://codecov.io/gh/ProjectMapK/KMapper) 5 | 6 | KMapper 7 | ==== 8 | `KMapper`は`Kotlin`向けのマッパーライブラリであり、以下の機能を提供します。 9 | 10 | - オブジェクトや`Map`、`Pair`をソースとした`Bean`マッピング 11 | - `Kotlin`のリフレクションを用いた関数呼び出しベースの安全なマッピング 12 | - 豊富な機能による、より柔軟かつ労力の少ないマッピング 13 | 14 | 以下のリポジトリに簡単なベンチマーク結果を掲載しています。 15 | 16 | - [ProjectMapK/MapKInspections: Testing and benchmarking for ProjectMapK deliverables\.](https://github.com/ProjectMapK/MapKInspections#results) 17 | 18 | ## デモコード 19 | 手動でマッピングコードを書いた場合と`KMapper`を用いた場合を比較します。 20 | 手動で書く場合引数が多ければ多いほど記述がかさみますが、`KMapper`を用いることで殆どコードを書かずにマッピングを行えます。 21 | また、外部の設定ファイルは一切必要ありません。 22 | 23 | ```kotlin 24 | // 手動でマッピングを行う場合 25 | val dst = Dst( 26 | param1 = src.param1, 27 | param2 = src.param2, 28 | param3 = src.param3, 29 | param4 = src.param4, 30 | param5 = src.param5, 31 | ... 32 | ) 33 | 34 | // KMapperを用いる場合 35 | val dst = KMapper(::Dst).map(src) 36 | ``` 37 | 38 | ソースは1つに限らず、複数のオブジェクトや、`Pair`、`Map`等を指定することもできます。 39 | 40 | ```kotlin 41 | val dst = KMapper(::Dst).map( 42 | "param1" to "value of param1", 43 | mapOf("param2" to 1, "param3" to 2L), 44 | src1, 45 | src2 46 | ) 47 | ``` 48 | 49 | ## インストール方法 50 | `KMapper`は`JitPack`にて公開しており、`Maven`や`Gradle`といったビルドツールから手軽に利用できます。 51 | 各ツールでの正確なインストール方法については下記をご参照ください。 52 | 53 | - [ProjectMapK / KMapper](https://jitpack.io/#ProjectMapK/KMapper) 54 | 55 | ### Mavenでのインストール方法 56 | 以下は`Maven`でのインストール例です。 57 | 58 | **1. JitPackのリポジトリへの参照を追加する** 59 | 60 | ```xml 61 | 62 | 63 | jitpack.io 64 | https://jitpack.io 65 | 66 | 67 | ``` 68 | 69 | **2. dependencyを追加する** 70 | 71 | ```xml 72 | 73 | com.github.ProjectMapK 74 | KMapper 75 | Tag 76 | 77 | ``` 78 | 79 | ## 動作原理 80 | `KMapper`は以下のように動作します。 81 | 82 | 1. 呼び出し対象の`KFunction`を取り出す 83 | 2. `KFunction`を解析し、必要な引数とその取り出し方を決定する 84 | 3. 入力からそれぞれの引数に対応する値の取り出しを行い、`KFunction`を呼び出す 85 | 86 | 最終的にはコンストラクタや`companion object`に定義したファクトリーメソッドなどを呼び出してマッピングを行うため、結果は`Kotlin`上の引数・`nullability`等の制約に従います。 87 | つまり、`Kotlin`の`null`安全が壊れることによる実行時エラーは発生しません(ただし、型引数の`nullability`に関しては`null`安全が壊れる場合が有ります)。 88 | 89 | また、`Kotlin`特有の機能であるデフォルト引数等にも対応しています。 90 | 91 | ## マッパークラスの種類について 92 | このプロジェクトでは以下の3種類のマッパークラスを提供しています。 93 | 94 | - `KMapper` 95 | - `PlainKMapper` 96 | - `BoundKMapper` 97 | 98 | 以下にそれぞれの特徴と使いどころをまとめます。 99 | また、これ以降共通の機能に関しては`KMapper`を例に説明を行います。 100 | 101 | ### KMapper 102 | `KMapper`はこのプロジェクトの基本となるマッパークラスです。 103 | 内部ではキャッシュを用いたマッピングの高速化などを行っているため、マッパーを使い回す形での利用に向きます。 104 | 105 | ### PlainKMapper 106 | `PlainKMapper`は`KMapper`からキャッシュ機能を取り除いたマッパークラスです。 107 | 複数回マッピングを行った場合の性能は`KMapper`に劣りますが、キャッシュ処理のオーバーヘッドが無いため、マッパーを使い捨てる形での利用に向きます。 108 | 109 | ### BoundKMapper 110 | `BoundKMapper`はソースとなるクラスが1つに限定できる場合に利用できるマッピングクラスです。 111 | `KMapper`に比べ高速に動作します。 112 | 113 | ## KMapperの初期化 114 | `KMapper`は呼び出し対象の`method reference(KFunction)`、またはマッピング先の`KClass`から初期化できます。 115 | 116 | 以下にそれぞれの初期化方法をまとめます。 117 | ただし、`BoundKMapper`の初期化の内可能なものは全てダミーコンストラクタによって簡略化した例を示します。 118 | 119 | ### method reference(KFunction)からの初期化 120 | プライマリコンストラクタを呼び出し対象とする場合、以下のように初期化を行うことができます。 121 | 122 | ```kotlin 123 | data class Dst( 124 | foo: String, 125 | bar: String, 126 | baz: Int?, 127 | 128 | ... 129 | 130 | ) 131 | 132 | // コンストラクタのメソッドリファレンスを取得 133 | val dstConstructor: KFunction = ::Dst 134 | 135 | // KMapperの場合 136 | val kMapper: KMapper = KMapper(dstConstructor) 137 | // PlainKMapperの場合 138 | val plainMapper: PlainKMapper = PlainKMapper(dstConstructor) 139 | // BoundKMapperの場合 140 | val boundKMapper: BoundKMapper = BoundKMapper(dstConstructor) 141 | ``` 142 | 143 | ### KClassからの初期化 144 | `KMapper`は`KClass`からも初期化できます。 145 | デフォルトではプライマリーコンストラクタが呼び出し対象になります。 146 | 147 | ```kotlin 148 | data class Dst(...) 149 | 150 | // KMapperの場合 151 | val kMapper: KMapper = KMapper(Dst::class) 152 | // PlainKMapperの場合 153 | val plainMapper: PlainKMapper = PlainKMapper(Dst::class) 154 | // BoundKMapperの場合 155 | val boundKMapper: BoundKMapper = BoundKMapper(Dst::class, Src::class) 156 | ``` 157 | 158 | ダミーコンストラクタを用い、かつジェネリクスを省略することで、それぞれ以下のようにも書けます。 159 | 160 | ```kotlin 161 | // KMapperの場合 162 | val kMapper: KMapper = KMapper() 163 | // PlainKMapperの場合 164 | val plainMapper: PlainKMapper = PlainKMapper() 165 | // BoundKMapperの場合 166 | val boundKMapper: BoundKMapper = BoundKMapper() 167 | ``` 168 | 169 | #### KConstructorアノテーションによる呼び出し対象指定 170 | `KClass`から初期化を行う場合、全てのマッパークラスでは`KConstructor`アノテーションを用いて呼び出し対象の関数を指定することができます。 171 | 172 | 以下の例ではセカンダリーコンストラクタが呼び出されます。 173 | 174 | ```kotlin 175 | data class Dst(...) { 176 | @KConstructor 177 | constructor(...) : this(...) 178 | } 179 | 180 | val mapper: KMapper = KMapper(Dst::class) 181 | ``` 182 | 183 | 同様に、以下の例ではファクトリーメソッドが呼び出されます。 184 | 185 | ```kotlin 186 | data class Dst(...) { 187 | companion object { 188 | @KConstructor 189 | fun factory(...): Dst { 190 | ... 191 | } 192 | } 193 | } 194 | 195 | val mapper: KMapper = KMapper(Dst::class) 196 | ``` 197 | 198 | ## 詳細な使い方 199 | 200 | ### マッピング時の値の変換 201 | マッピングを行うに当たり、入力の型を別の型に変換したい場合が有ります。 202 | `KMapper`では、そのような状況に対応するため、豊富な変換機能を提供しています。 203 | 204 | ただし、この変換処理は以下の条件でのみ行われます。 205 | 206 | - 入力が非`null` 207 | - `null`が絡む場合は`KParameterRequireNonNull`アノテーションとデフォルト引数を組み合わせることを推奨します 208 | - 入力が引数に直接代入できない 209 | 210 | #### デフォルトで利用可能な変換 211 | いくつかの変換機能は、特別な記述無しに利用することができます。 212 | 213 | ##### 1対1変換(ネストしたマッピング) 214 | 引数をそのまま用いることができず、かつその他の変換も行えない場合、`KMapper`は内部でマッピングクラスを用い、1対1マッピングを試みます。 215 | これによって、デフォルトで以下のようなネストしたマッピングを行うことができます。 216 | 217 | ```kotlin 218 | data class InnerDst(val foo: Int, val bar: Int) 219 | data class Dst(val param: InnerDst) 220 | 221 | data class InnerSrc(val foo: Int, val bar: Int) 222 | data class Src(val param: InnerSrc) 223 | 224 | val src = Src(InnerSrc(1, 2)) 225 | val dst = KMapper(::Dst).map(src) 226 | 227 | println(dst.param) // -> InnerDst(foo=1, bar=2) 228 | ``` 229 | 230 | ###### ネストしたマッピングに用いられる関数の指定 231 | ネストしたマッピングは、`BoundKMapper`をクラスから初期化して用いることで行われます。 232 | このため、`KConstructor`アノテーションを用いて呼び出し対象を指定することができます。 233 | 234 | ##### その他の変換 235 | 236 | ###### StringからEnumへの変換 237 | 入力が`String`で、かつ引数が`Enum`だった場合、入力と対応する`name`を持つ`Enum`への変換が試みられます。 238 | 239 | ```kotlin 240 | enum class FizzBuzz { 241 | Fizz, Buzz, FizzBuzz; 242 | } 243 | 244 | data class Dst(val fizzBuzz: FizzBuzz) 245 | 246 | val dst = KMapper(::Dst).map("fizzBuzz" to "Fizz") 247 | println(dst) // -> Dst(fizzBuzz=Fizz) 248 | ``` 249 | 250 | ###### Stringへの変換 251 | 引数が`String`だった場合、入力を`toString`する変換が行われます。 252 | 253 | #### KConverterアノテーションを設定することによる変換 254 | 自作のクラスで、かつ単一引数から初期化できる場合、`KConverter`アノテーションを用いた変換が利用できます。 255 | `KConverter`アノテーションは、コンストラクタ、もしくは`companion object`に定義したファクトリーメソッドに対して付与できます。 256 | 257 | ```kotlin 258 | // プライマリーコンストラクタに付与した場合 259 | data class FooId @KConverter constructor(val id: Int) 260 | ``` 261 | 262 | ```kotlin 263 | // セカンダリーコンストラクタに付与した場合 264 | data class FooId(val id: Int) { 265 | @KConverter 266 | constructor(id: String) : this(id.toInt()) 267 | } 268 | ``` 269 | 270 | ```kotlin 271 | // ファクトリーメソッドに付与した場合 272 | data class FooId(val id: Int) { 273 | companion object { 274 | @KConverter 275 | fun of(id: String): FooId = FooId(id.toInt()) 276 | } 277 | } 278 | ``` 279 | 280 | ```kotlin 281 | // fooIdにKConverterが付与されていればDstでは何もせずに正常にマッピングができる 282 | data class Dst( 283 | fooId: FooId, 284 | bar: String, 285 | baz: Int?, 286 | 287 | ... 288 | 289 | ) 290 | ``` 291 | 292 | #### コンバートアノテーションを自作しての変換 293 | 1対1の変換で`KConverter`を用いることができない場合、コンバートアノテーションを自作してパラメータに付与することで変換を行うことができます。 294 | 295 | コンバートアノテーションの自作はコンバートアノテーションとコンバータの組を定義することで行います。 296 | 例として`java.sql.Timestamp`もしくは`java.time.Instant`から指定したタイムゾーンの`ZonedDateTime`に変換を行う`ZonedDateTimeConverter`の作成の様子を示します。 297 | 298 | ##### コンバートアノテーションを定義する 299 | `@Target(AnnotationTarget.VALUE_PARAMETER)`と`KConvertBy`アノテーション、他幾つかのアノテーションを付与することで、コンバートアノテーションを定義できます。 300 | 301 | `KConvertBy`アノテーションの引数は、後述するコンバーターの`KClass`を渡します。 302 | このコンバーターはソースとなる型ごとに定義する必要があります。 303 | 304 | また、この例ではアノテーションに引数を定義していますが、この値はコンバーターから参照することができます。 305 | 306 | ```kotlin 307 | @Target(AnnotationTarget.VALUE_PARAMETER) 308 | @Retention(AnnotationRetention.RUNTIME) 309 | @MustBeDocumented 310 | @KConvertBy([TimestampToZonedDateTimeConverter::class, InstantToZonedDateTimeConverter::class]) 311 | annotation class ZonedDateTimeConverter(val zoneIdOf: String) 312 | ``` 313 | 314 | ##### コンバーターを定義する 315 | コンバーターは`AbstractKConverter`を継承して定義します。 316 | ジェネリクス`A`,`S`,`D`はそれぞれ以下の意味が有ります。 317 | - `A`: コンバートアノテーションの`Type` 318 | - `S`: 変換前の`Type` 319 | - `D`: 変換後の`Type` 320 | 321 | 以下は`java.sql.Timestamp`から`ZonedDateTime`へ変換を行うコンバーターの例です。 322 | 323 | ```kotlin 324 | class TimestampToZonedDateTimeConverter( 325 | annotation: ZonedDateTimeConverter 326 | ) : AbstractKConverter(annotation) { 327 | private val timeZone = ZoneId.of(annotation.zoneIdOf) 328 | 329 | override val srcClass: KClass = Timestamp::class 330 | 331 | override fun convert(source: Timestamp): ZonedDateTime = ZonedDateTime.of(source.toLocalDateTime(), timeZone) 332 | } 333 | ``` 334 | 335 | コンバーターのプライマリコンストラクタの引数はコンバートアノテーションのみ取る必要が有ります。 336 | これは`KMapper`の初期化時に呼び出されます。 337 | 338 | 例の通り、アノテーションに定義した引数は適宜参照することができます。 339 | 340 | ##### 付与する 341 | ここまでで定義したコンバートアノテーションとコンバーターをまとめて書くと以下のようになります。 342 | `InstantToZonedDateTimeConverter`は`java.time.Instant`をソースとするコンバーターです。 343 | 344 | ```kotlin 345 | @Target(AnnotationTarget.VALUE_PARAMETER) 346 | @Retention(AnnotationRetention.RUNTIME) 347 | @MustBeDocumented 348 | @KConvertBy([TimestampToZonedDateTimeConverter::class, InstantToZonedDateTimeConverter::class]) 349 | annotation class ZonedDateTimeConverter(val zoneIdOf: String) 350 | 351 | class TimestampToZonedDateTimeConverter( 352 | annotation: ZonedDateTimeConverter 353 | ) : AbstractKConverter(annotation) { 354 | private val timeZone = ZoneId.of(annotation.zoneIdOf) 355 | 356 | override val srcClass: KClass = Timestamp::class 357 | 358 | override fun convert(source: Timestamp): ZonedDateTime = ZonedDateTime.of(source.toLocalDateTime(), timeZone) 359 | } 360 | 361 | class InstantToZonedDateTimeConverter( 362 | annotation: ZonedDateTimeConverter 363 | ) : AbstractKConverter(annotation) { 364 | private val timeZone = ZoneId.of(annotation.zoneIdOf) 365 | 366 | override val srcClass: KClass = Instant::class 367 | 368 | override fun convert(source: Instant): ZonedDateTime = ZonedDateTime.ofInstant(source, timeZone) 369 | } 370 | ``` 371 | 372 | これを付与すると以下のようになります。 373 | 374 | ```kotlin 375 | data class Dst( 376 | @ZonedDateTimeConverter("Asia/Tokyo") 377 | val t1: ZonedDateTime, 378 | @ZonedDateTimeConverter("-03:00") 379 | val t2: ZonedDateTime 380 | ) 381 | ``` 382 | 383 | #### 複数引数からの変換 384 | 以下のような`Dst`で、`InnerDst`をマップ元の複数のフィールドから変換したい場合、`KParameterFlatten`アノテーションが利用できます。 385 | 386 | ```kotlin 387 | data class InnerDst(val fooFoo: Int, val barBar: String) 388 | data class Dst(val bazBaz: InnerDst, val quxQux: LocalDateTime) 389 | ``` 390 | 391 | `Dst`のフィールド名をプレフィックスに指定する場合以下のように付与します。 392 | ここで、`KParameterFlatten`を指定されたクラスは、前述の`KConstructor`アノテーションで指定した関数またはプライマリコンストラクタから初期化されます。 393 | 394 | ```kotlin 395 | data class InnerDst(val fooFoo: Int, val barBar: String) 396 | data class Dst( 397 | @KParameterFlatten 398 | val bazBaz: InnerDst, 399 | val quxQux: LocalDateTime 400 | ) 401 | data class Src(val bazBazFooBoo: Int, val bazBazBarBar: String, val quxQux: LocalDateTime) 402 | 403 | // bazBazFooFoo, bazBazBarBar, quxQuxの3引数が要求される 404 | val mapper = KMapper(::Dst) 405 | ``` 406 | 407 | ##### KParameterFlattenアノテーションのオプション 408 | `KParameterFlatten`アノテーションはネストしたクラスの引数名の扱いについて2つのオプションを持ちます。 409 | 410 | ###### fieldNameToPrefix 411 | `KParameterFlatten`アノテーションはデフォルトでは引数名をプレフィックスに置いた名前で一致を見ようとします。 412 | 引数名をプレフィックスに付けたくない場合は`fieldNameToPrefix`オプションに`false`を指定します。 413 | 414 | ```kotlin 415 | data class InnerDst(val fooFoo: Int, val barBar: String) 416 | data class Dst( 417 | @KParameterFlatten(fieldNameToPrefix = false) 418 | val bazBaz: InnerDst, 419 | val quxQux: LocalDateTime 420 | ) 421 | 422 | // fooFoo, barBar, quxQuxの3引数が要求される 423 | val mapper = KMapper(::Dst) 424 | ``` 425 | 426 | `fieldNameToPrefix = false`を指定した場合、`nameJoiner`オプションは無視されます。 427 | 428 | ###### nameJoiner 429 | `nameJoiner`は引数名と引数名の結合方法の指定です。 430 | 例えば`Src`が`snake_case`だった場合、以下のように利用します。 431 | 432 | ```kotlin 433 | data class InnerDst(val fooFoo: Int, val barBar: String) 434 | data class Dst( 435 | @KParameterFlatten(nameJoiner = NameJoiner.Snake::class) 436 | val bazBaz: InnerDst, 437 | val quxQux: LocalDateTime 438 | ) 439 | 440 | // baz_baz_foo_foo, baz_baz_bar_bar, qux_quxの3引数が要求される 441 | val mapper = KMapper(::Dst) { /* キャメル -> スネークの命名変換関数 */ } 442 | ``` 443 | 444 | デフォルトでは`camelCase`が指定されており、`snake_case`と`kebab-case`のサポートも有ります。 445 | `NameJoiner`クラスを継承した`object`を作成することで自作することもできます。 446 | 447 | ##### 他の変換方法との併用 448 | `KParameterFlatten`アノテーションを付与した場合も、これまでに紹介した変換方法は全て機能します。 449 | また、`KParameterFlatten`アノテーションは何重にネストした中でも利用が可能です。 450 | 451 | ### マッピング時に用いる引数名・フィールド名の設定 452 | `KMapper`は、デフォルトでは引数名に対応する名前のフィールドをソースからそのまま探します。 453 | 一方、引数名とソースで違う名前を用いたいという場合も有ります。 454 | 455 | `KMapper`では、そのような状況に対応するため、マッピング時に用いる引数名・フィールド名を設定するいくつかの機能を提供しています。 456 | 457 | #### 引数名の変換 458 | `KMapper`では、初期化時に引数名の変換関数を設定することができます。 459 | 例えば引数の命名規則がキャメルケースかつソースの命名規則がスネークケースというような、一定の変換が要求される状況に対応することができます。 460 | 461 | ```kotlin 462 | data class Dst( 463 | fooFoo: String, 464 | barBar: String, 465 | bazBaz: Int? 466 | ) 467 | 468 | val mapper: KMapper = KMapper(::Dst) { fieldName: String -> 469 | /* 命名変換処理 */ 470 | } 471 | 472 | // 例えばスネークケースへの変換関数を渡すことで、以下のような入力にも対応できる 473 | val dst = mapper.map(mapOf( 474 | "foo_foo" to "foo", 475 | "bar_bar" to "bar", 476 | "baz_baz" to 3 477 | )) 478 | ``` 479 | 480 | また、当然ながらラムダ内で任意の変換処理を行うこともできます。 481 | 482 | ##### 引数名の変換処理の伝播について 483 | 引数名の変換処理は、ネストしたマッピングにも反映されます。 484 | また、後述する`KParameterAlias`アノテーションで指定したエイリアスに関しても変換が適用されます。 485 | 486 | ##### 実際の変換処理 487 | `KMapper`では命名変換処理を提供していませんが、プロジェクトでよく用いられるライブラリでも命名変換処理が提供されている場合が有ります。 488 | `Jackson`、`Guava`の2つのライブラリで実際に「キャメルケース -> スネークケース」の変換処理を渡すサンプルコードを示します。 489 | 490 | ###### Jackson 491 | ```kotlin 492 | import com.fasterxml.jackson.databind.PropertyNamingStrategy 493 | 494 | val parameterNameConverter: (String) -> String = PropertyNamingStrategy.SnakeCaseStrategy()::translate 495 | val mapper: KMapper = KMapper(::Dst, parameterNameConverter) 496 | ``` 497 | 498 | ###### Guava 499 | ```kotlin 500 | import com.google.common.base.CaseFormat 501 | 502 | val parameterNameConverter: (String) -> String = { fieldName: String -> 503 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName) 504 | } 505 | val mapper: KMapper = KMapper(::Dst, parameterNameConverter) 506 | ``` 507 | 508 | #### ゲッターにエイリアスを設定する 509 | 以下のようなコードで、マッピング時にのみ`Scr`クラスの`_foo`フィールドの名前を変更する場合、`KGetterAlias`アノテーションを用いるのが最適です。 510 | 511 | ```kotlin 512 | data class Dst(val foo: Int) 513 | data class Src(val _foo: Int) 514 | ``` 515 | 516 | 実際に付与すると以下のようになります。 517 | 518 | ```kotlin 519 | data class Src( 520 | @get:KGetterAlias("foo") 521 | val _foo: Int 522 | ) 523 | ``` 524 | 525 | #### 引数名にエイリアスを設定する 526 | 以下のようなコードで、マッピング時にのみ`Dst`クラスの`_bar`フィールドの名前を変更する場合、`KParameterAlias`アノテーションを用いるのが最適です。 527 | 528 | ```kotlin 529 | data class Dst(val _bar: Int) 530 | data class Src(val bar: Int) 531 | ``` 532 | 533 | 実際に付与すると以下のようになります。 534 | 535 | ```kotlin 536 | data class Dst( 537 | @KParameterAlias("bar") 538 | val _bar: Int 539 | ) 540 | ``` 541 | 542 | ### その他機能 543 | #### 制御してデフォルト引数を用いる 544 | `KMapper`では、引数が指定されていなかった場合デフォルト引数を用います。 545 | また、引数が指定されていた場合でも、それを用いるか制御することができます。 546 | 547 | ##### 必ずデフォルト引数を用いる 548 | 必ずデフォルト引数を用いたい場合、`KUseDefaultArgument`アノテーションを利用できます。 549 | 550 | ```kotlin 551 | class Foo( 552 | ..., 553 | @KUseDefaultArgument 554 | val description: String = "" 555 | ) 556 | ``` 557 | 558 | ##### 対応する内容が全てnullの場合デフォルト引数を用いる 559 | `KParameterRequireNonNull`アノテーションを指定することで、引数として`non null`な値が指定されるまで入力をスキップします。 560 | これを利用することで、対応する内容が全て`null`の場合デフォルト引数を用いるという挙動が実現できます。 561 | 562 | ```kotlin 563 | class Foo( 564 | ..., 565 | @KParameterRequireNonNull 566 | val description: String = "" 567 | ) 568 | ``` 569 | 570 | #### マッピング時にフィールドを無視する 571 | 何らかの理由でマッピング時にフィールドを無視したい場合、`KGetterIgnore`アノテーションを用いることができます。 572 | 例えば、以下の`Src`クラスを入力した場合、`param1`フィールドは読み出し処理が行われません。 573 | 574 | ```kotlin 575 | data class Src( 576 | @KGetterIgnore 577 | val param1: Int, 578 | val param2: Int 579 | ) 580 | ``` 581 | 582 | ## 引数のセットアップ 583 | 584 | ### 引数読み出しの対象 585 | `KMapper`は、オブジェクトの`public`フィールド、もしくは`Pair`、`Map`のプロパティを読み出しの対象とすることができます。 586 | 587 | ### 引数のセットアップ 588 | `KMapper`は、値が`null`でなければセットアップ処理を行います。 589 | セットアップ処理では、まず`parameterClazz.isSuperclassOf(inputClazz)`で入力が引数に設定可能かを判定し、そのままでは設定できない場合は後述する変換処理を行い、結果を引数とします。 590 | 591 | 値が`null`だった場合は`KParameterRequireNonNull`アノテーションの有無を確認し、設定されていればセットアップ処理をスキップ、されていなければ`null`をそのまま引数とします。 592 | 593 | `KUseDefaultArgument`アノテーションが設定されていたり、`KParameterRequireNonNull`アノテーションによって全ての入力がスキップされた場合、デフォルト引数が用いられます。 594 | ここでデフォルト引数が利用できなかった場合は実行時エラーとなります。 595 | 596 | #### 引数の変換処理 597 | `KMapper`は、以下の順序で変換内容のチェック及び変換処理を行います。 598 | 599 | **1. アノテーションによる変換処理の指定の確認** 600 | まず初めに、入力のクラスに対応する、`KConvertBy`アノテーションや`KConverter`アノテーションによって指定された変換処理が無いかを確認します。 601 | 602 | **2. Enumへの変換可否の確認** 603 | 入力が`String`で、かつ引数が`Enum`だった場合、入力と対応する`name`を持つ`Enum`への変換を試みます。 604 | 605 | **3. 文字列への変換可否の確認** 606 | 引数が`String`の場合、入力を`toString`します。 607 | 608 | **4. マッパークラスを用いた変換処理** 609 | ここまでの変換条件に合致しなかった場合、マッパークラスを用いてネストした変換処理を行います。 610 | このマッピング処理には、`PlainKMapper`は`PlainKMapper`を、それ以外は`BoundKMapper`を用います。 611 | 612 | ### 入力の優先度 613 | `KMapper`では、基本的に先に入った入力可能な引数を優先します。 614 | 例えば、以下の例では`param1`として先に`value1`が指定されているため、`"param1" to "value2"`は無視されます。 615 | 616 | ```kotlin 617 | val mapper: KMapper = ... 618 | 619 | val dst = mapper.map("param1" to "value1", "param1" to "value2") 620 | ``` 621 | 622 | ただし、`KParameterRequireNonNull`アノテーションが指定された引数に対応する入力として`null`が指定された場合、その入力は無視され、後から入った引数が優先されます。 623 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 2 | [![CircleCI](https://circleci.com/gh/ProjectMapK/KMapper.svg?style=svg)](https://circleci.com/gh/ProjectMapK/KMapper) 3 | [![](https://jitci.com/gh/ProjectMapK/KMapper/svg)](https://jitci.com/gh/ProjectMapK/KMapper) 4 | [![codecov](https://codecov.io/gh/ProjectMapK/KMapper/branch/master/graph/badge.svg)](https://codecov.io/gh/ProjectMapK/KMapper) 5 | 6 | --- 7 | 8 | [日本語版](https://github.com/ProjectMapK/KMapper/blob/master/README.ja.md) 9 | 10 | --- 11 | 12 | KMapper 13 | ==== 14 | `KMapper` is a object to object mapper library for `Kotlin`, which provides the following features. 15 | 16 | - `Bean mapping` with `Objects`, `Map`, and `Pair` as sources 17 | - Flexible and safe mapping based on function calls with reflection. 18 | - Richer features and thus more flexible and labor-saving mapping. 19 | 20 | A brief benchmark result is posted in the following repository. 21 | 22 | - [ProjectMapK/MapKInspections: Testing and benchmarking for ProjectMapK deliverables\.](https://github.com/ProjectMapK/MapKInspections#results) 23 | 24 | ## Demo code 25 | Here is a comparison between writing the mapping code by manually and using `KMapper`. 26 | 27 | If you write it manually, the more arguments you have, the more complicated the description will be. 28 | However, by using `KMapper`, you can perform mapping without writing much code. 29 | 30 | Also, no external configuration file is required. 31 | 32 | ```kotlin 33 | // If you write manually. 34 | val dst = Dst( 35 | param1 = src.param1, 36 | param2 = src.param2, 37 | param3 = src.param3, 38 | param4 = src.param4, 39 | param5 = src.param5, 40 | ... 41 | ) 42 | 43 | // If you use KMapper 44 | val dst = KMapper(::Dst).map(src) 45 | ``` 46 | 47 | You can specify not only one source, but also multiple objects, `Pair`, `Map`, etc. 48 | 49 | ```kotlin 50 | val dst = KMapper(::Dst).map( 51 | "param1" to "value of param1", 52 | mapOf("param2" to 1, "param3" to 2L), 53 | src1, 54 | src2 55 | ) 56 | ``` 57 | 58 | ## Installation 59 | `KMapper` is published on JitPack. 60 | You can use this library on maven, gradle and any other build tools. 61 | Please see [here](https://jitpack.io/#ProjectMapK/KMapper/) for the introduction method. 62 | 63 | ### Example on maven 64 | **1. add repository reference for JitPack** 65 | 66 | ```xml 67 | 68 | 69 | jitpack.io 70 | https://jitpack.io 71 | 72 | 73 | ``` 74 | 75 | **2. add dependency** 76 | 77 | ```xml 78 | 79 | com.github.ProjectMapK 80 | KMapper 81 | Tag 82 | 83 | ``` 84 | 85 | ## Principle of operation 86 | The behavior of `KMapper` is as follows. 87 | 88 | 1. Get the `KFunction` to be called. 89 | 2. Analyze the `KFunction` and determine what arguments are needed and how to deserialize them. 90 | 3. Get the value for each argument from inputs and deserialize it. and call the `KFunction`. 91 | 92 | `KMapper` performs the mapping by calling a `function`, so the result is a Subject to the constraints on the `argument` and `nullability`. 93 | That is, there is no runtime error due to breaking the `null` safety of `Kotlin`(The `null` safety on type arguments may be broken due to problems on the `Kotlin` side). 94 | 95 | Also, it supports the default arguments which are peculiar to `Kotlin`. 96 | 97 | ## Types of mapper classes 98 | The project offers three types of mapper classes. 99 | 100 | - `KMapper` 101 | - `PlainKMapper` 102 | - `BoundKMapper` 103 | 104 | Here is a summary of the features and advantages of each. 105 | Also, the common features are explained using `KMapper` as an example. 106 | 107 | ### KMapper 108 | The `KMapper` is a basic mapper class for this project. 109 | It is suitable for using the same instance of the class, since it is cached internally to speed up the mapping process. 110 | 111 | ### PlainKMapper 112 | `PlainKMapper` is a mapper class from `KMapper` without caching. 113 | Although the performance is not as good as `KMapper` in case of multiple mappings, it is suitable for use as a disposable mapper because there is no overhead of cache processing. 114 | 115 | ### BoundKMapper 116 | `BoundKMapper` is a mapping class for the case where only one source class is available. 117 | It is faster than `KMapper`. 118 | 119 | ## Initialization 120 | `KMapper` can be initialized from `method reference(KFunction)` to be called or the `KClass` to be mapped. 121 | 122 | The following is a summary of each initialization. 123 | However, some of the initialization of `BoundKMapper` are shown as examples simplified by a dummy constructor. 124 | 125 | ### Initialize from method reference(KFunction) 126 | When the `primary constructor` is the target of a call, you can initialize it as follows. 127 | 128 | ```kotlin 129 | data class Dst( 130 | foo: String, 131 | bar: String, 132 | baz: Int?, 133 | 134 | ... 135 | 136 | ) 137 | 138 | // Get constructor reference 139 | val dstConstructor: KFunction = ::Dst 140 | 141 | // KMapper 142 | val kMapper: KMapper = KMapper(dstConstructor) 143 | // PlainKMapper 144 | val plainMapper: PlainKMapper = PlainKMapper(dstConstructor) 145 | // BoundKMapper 146 | val boundKMapper: BoundKMapper = BoundKMapper(dstConstructor) 147 | ``` 148 | 149 | ### Initialize from KClass 150 | The `KMapper` can also be initialized from the `KClass`. 151 | By default, the `primary constructor` is the target of the call. 152 | 153 | ```kotlin 154 | data class Dst(...) 155 | 156 | // KMapper 157 | val kMapper: KMapper = KMapper(Dst::class) 158 | // PlainKMapper 159 | val plainMapper: PlainKMapper = PlainKMapper(Dst::class) 160 | // BoundKMapper 161 | val boundKMapper: BoundKMapper = BoundKMapper(Dst::class, Src::class) 162 | ``` 163 | 164 | By using a `dummy constructor` and omitting `generics`, you can also write as follows. 165 | 166 | ```kotlin 167 | // KMapper 168 | val kMapper: KMapper = KMapper() 169 | // PlainKMapper 170 | val plainMapper: PlainKMapper = PlainKMapper() 171 | // BoundKMapper 172 | val boundKMapper: BoundKMapper = BoundKMapper() 173 | ``` 174 | 175 | ### Specifying the target of a call by KConstructor annotation 176 | When initializing from the `KClass`, all mapper classes can specify the function to be called by the `KConstructor` annotation. 177 | 178 | In the following example, the `secondary constructor` is called. 179 | 180 | ```kotlin 181 | data class Dst(...) { 182 | @KConstructor 183 | constructor(...) : this(...) 184 | } 185 | 186 | val mapper: KMapper = KMapper(Dst::class) 187 | ``` 188 | 189 | Similarly, the following example calls the factory method. 190 | 191 | ```kotlin 192 | data class Dst(...) { 193 | companion object { 194 | @KConstructor 195 | fun factory(...): Dst { 196 | ... 197 | } 198 | } 199 | } 200 | 201 | val mapper: KMapper = KMapper(Dst::class) 202 | ``` 203 | 204 | ## Detailed usage 205 | 206 | ### Converting values during mapping 207 | In mapping, you may want to convert one input type to another. 208 | The `KMapper` provides a rich set of conversion features for such a situation. 209 | 210 | However, this conversion can be performed under the following conditions. 211 | 212 | - Input is not `null`. 213 | - If `null` is involved, it is recommended to combine the `KParameterRequireNonNull` annotation with the default argument. 214 | - Input cannot be assigned directly to an argument. 215 | 216 | #### Conversions available by default 217 | Some of the conversion features are available without any special description. 218 | 219 | ##### 1-to-1 conversion (nested mapping) 220 | If you can't use arguments as they are and no other transformation is possible, `KMapper` tries to do 1-to-1 mapping using the mapping class. 221 | This allows you to perform the following nested mappings by default. 222 | 223 | ```kotlin 224 | data class InnerDst(val foo: Int, val bar: Int) 225 | data class Dst(val param: InnerDst) 226 | 227 | data class InnerSrc(val foo: Int, val bar: Int) 228 | data class Src(val param: InnerSrc) 229 | 230 | val src = Src(InnerSrc(1, 2)) 231 | val dst = KMapper(::Dst).map(src) 232 | 233 | println(dst.param) // -> InnerDst(foo=1, bar=2) 234 | ``` 235 | 236 | ###### Specifies the function used for the nested mapping 237 | Nested mapping is performed by initializing `BoundKMapper` from the class. 238 | For this reason, you can specify the target of the call with the `KConstructor` annotation. 239 | 240 | ##### Other conversions 241 | 242 | ###### Conversion from String to Enum 243 | If the input is a `String` and the argument is an `Enum`, an attempt is made to convert the input to an `Enum` with the corresponding `name`. 244 | 245 | ```kotlin 246 | enum class FizzBuzz { 247 | Fizz, Buzz, FizzBuzz; 248 | } 249 | 250 | data class Dst(val fizzBuzz: FizzBuzz) 251 | 252 | val dst = KMapper(::Dst).map("fizzBuzz" to "Fizz") 253 | println(dst) // -> Dst(fizzBuzz=Fizz) 254 | ``` 255 | 256 | ###### Conversion to String 257 | If the argument is a `String`, the input is converted by `toString` method. 258 | 259 | #### Specifying the conversion method using the KConverter annotation 260 | If you create your own class and can be initialized from a single argument, you can use the `KConverter` annotation. 261 | The `KConverter` annotation can be added to a `constructor` or a `factory method` defined in a `companion object`. 262 | 263 | ```kotlin 264 | // Annotate the primary constructor 265 | data class FooId @KConverter constructor(val id: Int) 266 | ``` 267 | 268 | ```kotlin 269 | // Annotate the secondary constructor 270 | data class FooId(val id: Int) { 271 | @KConverter 272 | constructor(id: String) : this(id.toInt()) 273 | } 274 | ``` 275 | 276 | ```kotlin 277 | // Annotate the factory method 278 | data class FooId(val id: Int) { 279 | companion object { 280 | @KConverter 281 | fun of(id: String): FooId = FooId(id.toInt()) 282 | } 283 | } 284 | ``` 285 | 286 | ```kotlin 287 | // If the fooId is given a KConverter, Dst can do the mapping successfully without doing anything. 288 | data class Dst( 289 | fooId: FooId, 290 | bar: String, 291 | baz: Int?, 292 | 293 | ... 294 | 295 | ) 296 | ``` 297 | 298 | #### Conversion by creating your own custom deserialization annotations 299 | If you cannot use `KConverter`, you can convert it by creating a custom conversion annotations and adding it to the parameter. 300 | 301 | Custom conversion annotation is made by defining a pair of `conversion annotation` and `converter`. 302 | As an example, we will show how to create a `ZonedDateTimeConverter` that converts from `java.sql.Timestamp` or `java.time.Instant` to `ZonedDateTime` in the specified time zone. 303 | 304 | ##### Create conversion annotation 305 | You can define a conversion annotation by adding `@Target(AnnotationTarget.VALUE_PARAMETER)`, `KConvertBy` annotation, and several other annotations. 306 | 307 | The argument of the `KConvertBy` annotation passes the `KClass` of the converter described below. 308 | This converter should be defined for each source type. 309 | 310 | Also, although this example defines an argument to the annotation, you can get the value of the annotation from the converter. 311 | 312 | ```kotlin 313 | @Target(AnnotationTarget.VALUE_PARAMETER) 314 | @Retention(AnnotationRetention.RUNTIME) 315 | @MustBeDocumented 316 | @KConvertBy([TimestampToZonedDateTimeConverter::class, InstantToZonedDateTimeConverter::class]) 317 | annotation class ZonedDateTimeConverter(val zoneIdOf: String) 318 | ``` 319 | 320 | ##### Create converter 321 | You can define `converter` by inheriting `AbstractKConverter`. 322 | Generics `A`,`S`,`D` have the following meanings. 323 | 324 | - `A`: `conversion annotation` `Type`. 325 | - `S`: Source `Type`. 326 | - `D`: Destination `Type`. 327 | 328 | Below is an example of a converter that converts from `java.sql.Timestamp` to `ZonedDateTime`. 329 | 330 | ```kotlin 331 | class TimestampToZonedDateTimeConverter( 332 | annotation: ZonedDateTimeConverter 333 | ) : AbstractKConverter(annotation) { 334 | private val timeZone = ZoneId.of(annotation.zoneIdOf) 335 | 336 | override val srcClass: KClass = Timestamp::class 337 | 338 | override fun convert(source: Timestamp): ZonedDateTime = ZonedDateTime.of(source.toLocalDateTime(), timeZone) 339 | } 340 | ``` 341 | 342 | The argument to the converter's `primary constructor` should only take a conversion annotation. 343 | This is called when `KMapper` is initialized. 344 | 345 | As shown in the example, you can refer to the arguments defined in the annotation. 346 | 347 | ##### Using custom conversion annotations 348 | The conversion annotation and the converter defined so far are written together as follows. 349 | `InstantToZonedDateTimeConverter` is a converter whose source is `java.time.Instant`. 350 | 351 | ```kotlin 352 | @Target(AnnotationTarget.VALUE_PARAMETER) 353 | @Retention(AnnotationRetention.RUNTIME) 354 | @MustBeDocumented 355 | @KConvertBy([TimestampToZonedDateTimeConverter::class, InstantToZonedDateTimeConverter::class]) 356 | annotation class ZonedDateTimeConverter(val zoneIdOf: String) 357 | 358 | class TimestampToZonedDateTimeConverter( 359 | annotation: ZonedDateTimeConverter 360 | ) : AbstractKConverter(annotation) { 361 | private val timeZone = ZoneId.of(annotation.zoneIdOf) 362 | 363 | override val srcClass: KClass = Timestamp::class 364 | 365 | override fun convert(source: Timestamp): ZonedDateTime = ZonedDateTime.of(source.toLocalDateTime(), timeZone) 366 | } 367 | 368 | class InstantToZonedDateTimeConverter( 369 | annotation: ZonedDateTimeConverter 370 | ) : AbstractKConverter(annotation) { 371 | private val timeZone = ZoneId.of(annotation.zoneIdOf) 372 | 373 | override val srcClass: KClass = Instant::class 374 | 375 | override fun convert(source: Instant): ZonedDateTime = ZonedDateTime.ofInstant(source, timeZone) 376 | } 377 | ``` 378 | 379 | When this is given, it becomes as follows. 380 | 381 | ```kotlin 382 | data class Dst( 383 | @ZonedDateTimeConverter("Asia/Tokyo") 384 | val t1: ZonedDateTime, 385 | @ZonedDateTimeConverter("-03:00") 386 | val t2: ZonedDateTime 387 | ) 388 | ``` 389 | 390 | #### Conversion from Multiple Arguments 391 | The `KParameterFlatten` annotation allows you to perform a transformation that requires more than one argument. 392 | 393 | ```kotlin 394 | data class InnerDst(val fooFoo: Int, val barBar: String) 395 | data class Dst(val bazBaz: InnerDst, val quxQux: LocalDateTime) 396 | ``` 397 | 398 | To specify a field name as a prefix, give it as follows. 399 | The class specified with `KParameterFlatten` is initialized from the function or the `primary constructor` specified with the aforementioned `KConstructor` annotation. 400 | 401 | ```kotlin 402 | data class InnerDst(val fooFoo: Int, val barBar: String) 403 | data class Dst( 404 | @KParameterFlatten 405 | val bazBaz: InnerDst, 406 | val quxQux: LocalDateTime 407 | ) 408 | data class Src(val bazBazFooBoo: Int, val bazBazBarBar: String, val quxQux: LocalDateTime) 409 | 410 | // required 3 arguments that bazBazFooFoo, bazBazBarBar, quxQux 411 | val mapper = KMapper(::Dst) 412 | ``` 413 | 414 | ##### KParameterFlatten annotation options 415 | The `KParameterFlatten` annotation has two options for handling argument names of the nested classes. 416 | 417 | ###### fieldNameToPrefix 418 | By default, the `KParameterFlatten` annotation tries to find a match by prefixing the name of the argument with the name of the prefix. 419 | If you don't want to prefix the argument names, you can set the `fieldNameToPrefix` option to `false`. 420 | 421 | ```kotlin 422 | data class InnerDst(val fooFoo: Int, val barBar: String) 423 | data class Dst( 424 | @KParameterFlatten(fieldNameToPrefix = false) 425 | val bazBaz: InnerDst, 426 | val quxQux: LocalDateTime 427 | ) 428 | 429 | // required 3 arguments that fooFoo, barBar, quxQux 430 | val mapper = KMapper(::Dst) 431 | ``` 432 | 433 | If `fieldNameToPrefix = false` is specified, the `nameJoiner` option is ignored. 434 | 435 | ###### nameJoiner 436 | The `nameJoiner` specifies how to join argument names and argument names. 437 | For example, if `Src` is `snake_case`, the following command is used. 438 | 439 | ```kotlin 440 | data class InnerDst(val fooFoo: Int, val barBar: String) 441 | data class Dst( 442 | @KParameterFlatten(nameJoiner = NameJoiner.Snake::class) 443 | val bazBaz: InnerDst, 444 | val quxQux: LocalDateTime 445 | ) 446 | 447 | // required 3 arguments that baz_baz_foo_foo, baz_baz_bar_bar, qux_qux 448 | val mapper = KMapper(::Dst) { /* some naming transformation process */ } 449 | ``` 450 | 451 | By default, `camelCase` is specified, and `snake_case` and `kebab-case` are also supported. 452 | You can also write your own by creating `object` which extends the `NameJoiner` class. 453 | 454 | ##### Use with other conversion methods 455 | The `KParameterFlatten` annotation also works with all the conversion methods introduced so far. 456 | Also, the `KParameterFlatten` annotation can be used in any number of layers of nested objects. 457 | 458 | ### Set the argument names and field names used for mapping 459 | By default, `KMapper` searches the source for a field whose name corresponds to the argument name. 460 | On the other hand, there are times when you want to use a different name for the argument name and the source. 461 | 462 | In order to deal with such a situation, `KMapper` provides some functions to set the argument name and field name used during mapping. 463 | 464 | #### Conversion of argument names 465 | With `KMapper`, you can set the argument name conversion function at initialization. 466 | It can handle situations where constant conversion is required, for example, the argument naming convention is camel case and the source naming convention is snake case. 467 | 468 | ```kotlin 469 | data class Dst( 470 | fooFoo: String, 471 | barBar: String, 472 | bazBaz: Int? 473 | ) 474 | 475 | val mapper: KMapper = KMapper(::Dst) { fieldName: String -> 476 | /* some naming transformation process */ 477 | } 478 | 479 | // For example, by passing a conversion function to the snake case, the following input can be handled 480 | val dst = mapper.map(mapOf( 481 | "foo_foo" to "foo", 482 | "bar_bar" to "bar", 483 | "baz_baz" to 3 484 | )) 485 | ``` 486 | 487 | And, of course, any conversion process can be performed within the lambda. 488 | 489 | ##### Propagation of the argument name conversion process 490 | The argument name conversion process is also reflected in the nested mapping. 491 | Also, the conversion is applied to the aliases specified with the `KParameterAlias` annotation described below. 492 | 493 | ##### The actual conversion process 494 | Although `KMapper` does not provide naming transformation, some of the most popular libraries in your project may also provide it. 495 | Here is a sample code of `Jackson` and `Guava` that actually passes the "CamelCase -> SnakeCase" transformations. 496 | 497 | ##### Jackson 498 | ```kotlin 499 | import com.fasterxml.jackson.databind.PropertyNamingStrategy 500 | 501 | val parameterNameConverter: (String) -> String = PropertyNamingStrategy.SnakeCaseStrategy()::translate 502 | val mapper: KMapper = KMapper(::Dst, parameterNameConverter) 503 | ``` 504 | 505 | ##### Guava 506 | ```kotlin 507 | import com.google.common.base.CaseFormat 508 | 509 | val parameterNameConverter: (String) -> String = { fieldName: String -> 510 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName) 511 | } 512 | val mapper: KMapper = KMapper(::Dst, parameterNameConverter) 513 | ``` 514 | 515 | #### Set an alias for the getter 516 | It is best to use the `KGetterAlias` annotation to rename the `_foo` field of the `Scr` class only at mapping time in the following code. 517 | 518 | ```kotlin 519 | data class Dst(val foo: Int) 520 | data class Src(val _foo: Int) 521 | ``` 522 | 523 | The actual grant is as follows. 524 | 525 | ```kotlin 526 | data class Src( 527 | @get:KGetterAlias("foo") 528 | val _foo: Int 529 | ) 530 | ``` 531 | 532 | #### Set an alias to an argument name 533 | It is best to use the `KParameterAlias` annotation if you want to change the name of the `_bar` field of the `Dst` class only at mapping time in the following code. 534 | 535 | ```kotlin 536 | data class Dst(val _bar: Int) 537 | data class Src(val bar: Int) 538 | ``` 539 | 540 | The actual grant is as follows. 541 | 542 | ```kotlin 543 | data class Dst( 544 | @KParameterAlias("bar") 545 | val _bar: Int 546 | ) 547 | ``` 548 | 549 | ### Other functions 550 | #### Control and use default arguments 551 | The `KMapper` uses the default argument if no argument is given. 552 | Also, if an argument is given, you can control whether to use it or not. 553 | 554 | ##### Always use the default arguments 555 | If you want to force a default argument, you can use the `KUseDefaultArgument` annotation. 556 | 557 | ```kotlin 558 | class Foo( 559 | ..., 560 | @KUseDefaultArgument 561 | val description: String = "" 562 | ) 563 | ``` 564 | 565 | ##### Use default argument if input is null 566 | The `KParameterRequireNonNull` annotation skips the input until a `non null` value is specified as an argument. 567 | By using this, the default argument is used when all the corresponding contents are `null`. 568 | 569 | ```kotlin 570 | class Foo( 571 | ..., 572 | @KParameterRequireNonNull 573 | val description: String = "" 574 | ) 575 | ``` 576 | 577 | #### Ignore the field when mapping 578 | If you want to ignore a field for mapping for some reason, you can use the `KGetterIgnore` annotation. 579 | For example, if you enter the following class of `Src`, the `param1` field will not be read. 580 | 581 | ```kotlin 582 | data class Src( 583 | @KGetterIgnore 584 | val param1: Int, 585 | val param2: Int 586 | ) 587 | ``` 588 | 589 | ## Setting Up Arguments 590 | 591 | ### Target for argument reading 592 | The `KMapper` can read the `public` field of an object, or the properties of `Pair` and `Map`. 593 | 594 | ### Setting Up Arguments 595 | The `KMapper` performs the setup process if the value is not `null`. 596 | In the setup process, first of all, `parameterClazz.isSuperclassOf(inputClazz)` is used to check if the input can be set as an argument or not, and if not, the conversion described later is performed and the result is used as an argument. 597 | 598 | If the value is `null`, the `KParameterRequireNonNull` annotation is checked, and if it is set, the setup process is skipped, otherwise `null` is used as the argument. 599 | 600 | If the `KUseDefaultArgument` annotation is set or all inputs are skipped by the `KParameterRequireNonNull` annotation, the default argument is used. 601 | If the default argument is not available at this time, a runtime error occurs. 602 | 603 | #### Conversion of arguments 604 | `KMapper` performs conversion and checking in the following order. 605 | 606 | **1. Checking the specification of the conversion process by annotation** 607 | First of all, it checks for conversions specified by the `KConvertBy` and `KConverter` annotations for the class of the input. 608 | 609 | **2. Confirmation of conversion to Enum** 610 | If the input is a `String` and the argument is an `Enum`, the function tries to convert the input to an `Enum` with the corresponding `name`. 611 | 612 | **3. Confirmation of conversion to string** 613 | If the argument is `String`, the input will be `toString`. 614 | 615 | **4. Conversion using the mapper class** 616 | If the transformation does not meet the criteria so far, a mapping process is performed using a mapper class. 617 | For this mapping process, `PlainKMapper` is used for `PlainKMapper`, and `BoundKMapper` is used for others. 618 | 619 | ### Input priority 620 | The `KMapper` basically gives priority to the first available argument. 621 | For example, in the following example, since `param1` is given first as `value1`, the next input `param1" to "value2"` is ignored. 622 | 623 | ```kotlin 624 | val mapper: KMapper = ... 625 | 626 | val dst = mapper.map("param1" to "value1", "param1" to "value2") 627 | ``` 628 | 629 | However, if `null` is specified as an input for an argument with a `KParameterRequireNonNull` annotation, it is ignored and the later argument takes precedence. 630 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("maven") 3 | id("java") 4 | kotlin("jvm") version "1.4.32" 5 | // その他補助系 6 | id("org.jlleitschuh.gradle.ktlint") version "10.0.0" 7 | id("jacoco") 8 | id("com.github.ben-manes.versions") version "0.28.0" 9 | } 10 | 11 | group = "com.mapk" 12 | version = "0.36" 13 | 14 | java { 15 | sourceCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | repositories { 19 | mavenCentral() 20 | maven { setUrl("https://jitpack.io") } 21 | } 22 | 23 | dependencies { 24 | implementation(kotlin("reflect")) 25 | api("com.github.ProjectMapK:Shared:0.20") 26 | 27 | // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter 28 | testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.7.1") { 29 | exclude(group = "org.junit.vintage", module = "junit-vintage-engine") 30 | } 31 | // 現状プロパティ名の変換はテストでしか使っていないのでtestImplementation 32 | // https://mvnrepository.com/artifact/com.google.guava/guava 33 | testImplementation(group = "com.google.guava", name = "guava", version = "29.0-jre") 34 | } 35 | 36 | tasks { 37 | compileKotlin { 38 | dependsOn("ktlintFormat") 39 | kotlinOptions { 40 | jvmTarget = "1.8" 41 | allWarningsAsErrors = true 42 | } 43 | } 44 | 45 | compileTestKotlin { 46 | kotlinOptions { 47 | freeCompilerArgs = listOf("-Xjsr305=strict") 48 | jvmTarget = "1.8" 49 | } 50 | } 51 | test { 52 | useJUnitPlatform() 53 | // テスト終了時にjacocoのレポートを生成する 54 | finalizedBy(jacocoTestReport) 55 | } 56 | 57 | jacocoTestReport { 58 | reports { 59 | xml.isEnabled = true 60 | csv.isEnabled = false 61 | html.isEnabled = true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.warning.mode=all 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMapK/KMapper/15ccd09d6b9f9c46b8fd9ace2324016a246d69b5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "KMapper" 2 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/annotations/KConverter.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.annotations 2 | 3 | @Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | @MustBeDocumented 6 | annotation class KConverter 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/annotations/KGetterAlias.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.annotations 2 | 3 | @Target(AnnotationTarget.PROPERTY_GETTER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | @MustBeDocumented 6 | annotation class KGetterAlias(val value: String) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/annotations/KGetterIgnore.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.annotations 2 | 3 | @Target(AnnotationTarget.PROPERTY_GETTER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | @MustBeDocumented 6 | annotation class KGetterIgnore 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/conversion/KConvert.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.conversion 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @Target(AnnotationTarget.ANNOTATION_CLASS) 6 | @Retention(AnnotationRetention.RUNTIME) 7 | @MustBeDocumented 8 | annotation class KConvertBy(val converters: Array>>) 9 | 10 | abstract class AbstractKConverter(protected val annotation: A) { 11 | abstract val srcClass: KClass 12 | abstract fun convert(source: S): D? 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KGetterAlias 4 | import com.mapk.annotations.KGetterIgnore 5 | import com.mapk.core.KFunctionForCall 6 | import com.mapk.core.toKConstructor 7 | import java.lang.IllegalArgumentException 8 | import kotlin.reflect.KClass 9 | import kotlin.reflect.KFunction 10 | import kotlin.reflect.KProperty1 11 | import kotlin.reflect.KVisibility 12 | import kotlin.reflect.full.findAnnotation 13 | import kotlin.reflect.full.memberProperties 14 | import kotlin.reflect.jvm.jvmName 15 | 16 | class BoundKMapper private constructor( 17 | private val function: KFunctionForCall, 18 | src: KClass, 19 | parameterNameConverter: ((String) -> String)? 20 | ) { 21 | constructor(function: KFunction, src: KClass, parameterNameConverter: ((String) -> String)? = null) : this( 22 | KFunctionForCall(function, parameterNameConverter), 23 | src, 24 | parameterNameConverter 25 | ) 26 | 27 | constructor(clazz: KClass, src: KClass, parameterNameConverter: ((String) -> String)? = null) : this( 28 | clazz.toKConstructor(parameterNameConverter), 29 | src, 30 | parameterNameConverter 31 | ) 32 | 33 | private val parameters: List> 34 | 35 | init { 36 | val srcPropertiesMap: Map> = src.memberProperties 37 | .filter { 38 | // アクセス可能かつignoreされてないもののみ抽出 39 | it.visibility == KVisibility.PUBLIC && it.getter.annotations.none { annotation -> annotation is KGetterIgnore } 40 | }.associateBy { it.getter.findAnnotation()?.value ?: it.name } 41 | 42 | parameters = function.requiredParameters 43 | .mapNotNull { 44 | srcPropertiesMap[it.name]?.let { property -> 45 | BoundParameterForMap.newInstance(it, property, parameterNameConverter) 46 | }.apply { 47 | // 必須引数に対応するプロパティがsrcに定義されていない場合エラー 48 | if (this == null && !it.isOptional) 49 | throw IllegalArgumentException("Property ${it.name} is not declared in ${src.jvmName}.") 50 | } 51 | } 52 | } 53 | 54 | fun map(src: S): D { 55 | val adaptor = function.getArgumentAdaptor() 56 | 57 | parameters.forEach { 58 | adaptor.forcePut(it.name, it.map(src)) 59 | } 60 | 61 | return function.call(adaptor) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.core.EnumMapper 4 | import com.mapk.core.ValueParameter 5 | import java.lang.IllegalArgumentException 6 | import java.lang.reflect.Method 7 | import kotlin.reflect.KClass 8 | import kotlin.reflect.KFunction 9 | import kotlin.reflect.KProperty1 10 | import kotlin.reflect.full.isSubclassOf 11 | import kotlin.reflect.jvm.javaGetter 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | internal sealed class BoundParameterForMap { 15 | abstract val name: String 16 | protected abstract val propertyGetter: Method 17 | 18 | abstract fun map(src: S): Any? 19 | 20 | internal class Plain( 21 | override val name: String, 22 | override val propertyGetter: Method 23 | ) : BoundParameterForMap() { 24 | override fun map(src: S): Any? = propertyGetter.invoke(src) 25 | } 26 | 27 | internal class UseConverter( 28 | override val name: String, 29 | override val propertyGetter: Method, 30 | private val converter: KFunction<*> 31 | ) : BoundParameterForMap() { 32 | override fun map(src: S): Any? = propertyGetter.invoke(src)?.let { converter.call(it) } 33 | } 34 | 35 | internal class UseKMapper( 36 | override val name: String, 37 | override val propertyGetter: Method, 38 | private val kMapper: KMapper<*> 39 | ) : BoundParameterForMap() { 40 | // 1引数で呼び出すとMap/Pairが適切に処理されないため、2引数目にダミーを噛ませている 41 | override fun map(src: S): Any? = propertyGetter.invoke(src)?.let { kMapper.map(it, PARAMETER_DUMMY) } 42 | } 43 | 44 | internal class UseBoundKMapper( 45 | override val name: String, 46 | override val propertyGetter: Method, 47 | private val boundKMapper: BoundKMapper 48 | ) : BoundParameterForMap() { 49 | override fun map(src: S): Any? = (propertyGetter.invoke(src))?.let { boundKMapper.map(it as T) } 50 | } 51 | 52 | internal class ToEnum( 53 | override val name: String, 54 | override val propertyGetter: Method, 55 | private val paramClazz: Class<*> 56 | ) : BoundParameterForMap() { 57 | override fun map(src: S): Any? = EnumMapper.getEnum(paramClazz, propertyGetter.invoke(src) as String?) 58 | } 59 | 60 | internal class ToString( 61 | override val name: String, 62 | override val propertyGetter: Method 63 | ) : BoundParameterForMap() { 64 | override fun map(src: S): String? = propertyGetter.invoke(src)?.toString() 65 | } 66 | 67 | companion object { 68 | fun newInstance( 69 | param: ValueParameter<*>, 70 | property: KProperty1, 71 | parameterNameConverter: ((String) -> String)? 72 | ): BoundParameterForMap { 73 | // ゲッターが無いならエラー 74 | val propertyGetter = property.javaGetter 75 | ?: throw IllegalArgumentException("${property.name} does not have getter.") 76 | propertyGetter.isAccessible = true 77 | 78 | val paramClazz = param.requiredClazz 79 | val propertyClazz = property.returnType.classifier as KClass<*> 80 | 81 | // コンバータが取れた場合 82 | (param.getConverters() + paramClazz.getConverters()) 83 | .filter { (key, _) -> propertyClazz.isSubclassOf(key) } 84 | .let { 85 | if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it") 86 | 87 | it.singleOrNull()?.let { (_, converter) -> 88 | return UseConverter(param.name, propertyGetter, converter) 89 | } 90 | } 91 | 92 | if (paramClazz.isSubclassOf(propertyClazz)) { 93 | return Plain(param.name, propertyGetter) 94 | } 95 | 96 | val javaClazz = paramClazz.java 97 | 98 | return when { 99 | javaClazz.isEnum && propertyClazz == String::class -> ToEnum(param.name, propertyGetter, javaClazz) 100 | paramClazz == String::class -> ToString(param.name, propertyGetter) 101 | // SrcがMapやPairならKMapperを使わないとマップできない 102 | propertyClazz.isSubclassOf(Map::class) || propertyClazz.isSubclassOf(Pair::class) -> UseKMapper( 103 | param.name, 104 | propertyGetter, 105 | KMapper(paramClazz, parameterNameConverter) 106 | ) 107 | // 何にも当てはまらなければBoundKMapperでマップを試みる 108 | else -> UseBoundKMapper( 109 | param.name, 110 | propertyGetter, 111 | BoundKMapper(paramClazz, propertyClazz, parameterNameConverter) 112 | ) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/DummyConstructor.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionName") 2 | 3 | package com.mapk.kmapper 4 | 5 | import kotlin.reflect.KFunction 6 | import com.mapk.kmapper.BoundKMapper as Bound 7 | import com.mapk.kmapper.KMapper as Normal 8 | import com.mapk.kmapper.PlainKMapper as Plain 9 | 10 | inline fun BoundKMapper(): Bound = Bound(D::class, S::class) 11 | 12 | inline fun BoundKMapper( 13 | noinline parameterNameConverter: ((String) -> String) 14 | ): Bound = Bound(D::class, S::class, parameterNameConverter) 15 | 16 | inline fun BoundKMapper(function: KFunction): Bound = Bound(function, S::class) 17 | 18 | inline fun BoundKMapper( 19 | function: KFunction, 20 | noinline parameterNameConverter: ((String) -> String) 21 | ): Bound = Bound(function, S::class, parameterNameConverter) 22 | 23 | inline fun KMapper(): Normal = Normal(T::class) 24 | 25 | inline fun KMapper(noinline parameterNameConverter: ((String) -> String)): Normal = 26 | Normal(T::class, parameterNameConverter) 27 | 28 | inline fun PlainKMapper(): Plain = Plain(T::class) 29 | 30 | inline fun PlainKMapper(noinline parameterNameConverter: ((String) -> String)): Plain = 31 | Plain(T::class, parameterNameConverter) 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/KMapper.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KGetterAlias 4 | import com.mapk.annotations.KGetterIgnore 5 | import com.mapk.core.ArgumentAdaptor 6 | import com.mapk.core.KFunctionForCall 7 | import com.mapk.core.toKConstructor 8 | import java.lang.reflect.Method 9 | import java.util.concurrent.ConcurrentHashMap 10 | import java.util.concurrent.ConcurrentMap 11 | import kotlin.reflect.KClass 12 | import kotlin.reflect.KFunction 13 | import kotlin.reflect.KVisibility 14 | import kotlin.reflect.full.memberProperties 15 | import kotlin.reflect.jvm.javaGetter 16 | 17 | class KMapper private constructor( 18 | private val function: KFunctionForCall, 19 | parameterNameConverter: ((String) -> String)? 20 | ) { 21 | constructor(function: KFunction, parameterNameConverter: ((String) -> String)? = null) : this( 22 | KFunctionForCall(function, parameterNameConverter), 23 | parameterNameConverter 24 | ) 25 | 26 | constructor(clazz: KClass, parameterNameConverter: ((String) -> String)? = null) : this( 27 | clazz.toKConstructor(parameterNameConverter), 28 | parameterNameConverter 29 | ) 30 | 31 | private val parameterMap: Map> = function.requiredParameters.associate { 32 | it.name to ParameterForMap(it, parameterNameConverter) 33 | } 34 | 35 | private val getCache: ConcurrentMap, List> = ConcurrentHashMap() 36 | 37 | private fun bindArguments(argumentAdaptor: ArgumentAdaptor, src: Any) { 38 | val clazz = src::class 39 | 40 | // キャッシュヒットしたら登録した内容に沿って取得処理を行う 41 | getCache[clazz]?.let { getters -> 42 | // 取得対象フィールドは十分絞り込んでいると考えられるため、終了判定は行わない 43 | getters.forEach { it.bindArgument(src, argumentAdaptor) } 44 | return 45 | } 46 | 47 | val tempBinderArrayList = ArrayList() 48 | 49 | clazz.memberProperties.forEach outer@{ property -> 50 | // propertyが公開されていない場合は処理を行わない 51 | if (property.visibility != KVisibility.PUBLIC) return@outer 52 | 53 | // ゲッターが取れない場合は処理を行わない 54 | val javaGetter: Method = property.javaGetter ?: return@outer 55 | 56 | var alias: String? = null 57 | // NOTE: IgnoreとAliasが同時に指定されるようなパターンを考慮してaliasが取れてもbreakしていない 58 | javaGetter.annotations.forEach { 59 | if (it is KGetterIgnore) return@outer // ignoreされている場合は処理を行わない 60 | if (it is KGetterAlias) alias = it.value 61 | } 62 | 63 | parameterMap[alias ?: property.name]?.let { param -> 64 | javaGetter.isAccessible = true 65 | 66 | val binder = ArgumentBinder(param, javaGetter) 67 | 68 | binder.bindArgument(src, argumentAdaptor) 69 | tempBinderArrayList.add(binder) 70 | // キャッシュの整合性を保つため、ここでは終了判定を行わない 71 | } 72 | } 73 | getCache.putIfAbsent(clazz, tempBinderArrayList) 74 | } 75 | 76 | private fun bindArguments(argumentAdaptor: ArgumentAdaptor, src: Map<*, *>) { 77 | src.forEach { (key, value) -> 78 | parameterMap[key]?.let { param -> 79 | // 取得した内容がnullでなければ適切にmapする 80 | argumentAdaptor.putIfAbsent(param.name) { value?.let { param.mapObject(value) } } 81 | // 終了判定 82 | if (argumentAdaptor.isFullInitialized()) return 83 | } 84 | } 85 | } 86 | 87 | private fun bindArguments(argumentAdaptor: ArgumentAdaptor, srcPair: Pair<*, *>) { 88 | val key = srcPair.first.toString() 89 | 90 | parameterMap[key]?.let { 91 | argumentAdaptor.putIfAbsent(key) { srcPair.second?.let { value -> it.mapObject(value) } } 92 | } 93 | } 94 | 95 | fun map(srcMap: Map): T { 96 | val adaptor: ArgumentAdaptor = function.getArgumentAdaptor() 97 | bindArguments(adaptor, srcMap) 98 | 99 | return function.call(adaptor) 100 | } 101 | 102 | fun map(srcPair: Pair): T { 103 | val adaptor: ArgumentAdaptor = function.getArgumentAdaptor() 104 | bindArguments(adaptor, srcPair) 105 | 106 | return function.call(adaptor) 107 | } 108 | 109 | fun map(src: Any): T { 110 | val adaptor: ArgumentAdaptor = function.getArgumentAdaptor() 111 | bindArguments(adaptor, src) 112 | 113 | return function.call(adaptor) 114 | } 115 | 116 | fun map(vararg args: Any): T { 117 | val adaptor: ArgumentAdaptor = function.getArgumentAdaptor() 118 | 119 | listOf(*args).forEach { arg -> 120 | when (arg) { 121 | is Map<*, *> -> bindArguments(adaptor, arg) 122 | is Pair<*, *> -> bindArguments(adaptor, arg) 123 | else -> bindArguments(adaptor, arg) 124 | } 125 | } 126 | 127 | return function.call(adaptor) 128 | } 129 | } 130 | 131 | private class ArgumentBinder(private val param: ParameterForMap<*>, private val javaGetter: Method) { 132 | fun bindArgument(src: Any, adaptor: ArgumentAdaptor) { 133 | adaptor.putIfAbsent(param.name) { 134 | javaGetter.invoke(src)?.let { param.mapObject(it) } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.core.EnumMapper 4 | import com.mapk.core.ValueParameter 5 | import java.util.concurrent.ConcurrentHashMap 6 | import java.util.concurrent.ConcurrentMap 7 | import kotlin.reflect.KClass 8 | import kotlin.reflect.KFunction 9 | import kotlin.reflect.full.isSuperclassOf 10 | 11 | internal class ParameterForMap( 12 | param: ValueParameter, 13 | private val parameterNameConverter: ((String) -> String)? 14 | ) { 15 | val name: String = param.name 16 | private val clazz: KClass = param.requiredClazz 17 | 18 | private val javaClazz: Class by lazy { 19 | clazz.java 20 | } 21 | // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい 22 | @Suppress("UNCHECKED_CAST") 23 | private val converters: Set, KFunction>> by lazy { 24 | (param.getConverters() as Set, KFunction>>) + clazz.getConverters() 25 | } 26 | 27 | private val convertCache: ConcurrentMap, ParameterProcessor> = ConcurrentHashMap() 28 | 29 | fun mapObject(value: U): Any? { 30 | val valueClazz: KClass<*> = value::class 31 | 32 | // 取得方法のキャッシュが有ればそれを用いる 33 | convertCache[valueClazz]?.let { return it.process(value) } 34 | 35 | // パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる 36 | if (clazz.isSuperclassOf(valueClazz)) { 37 | convertCache.putIfAbsent(valueClazz, ParameterProcessor.Plain) 38 | return value 39 | } 40 | 41 | val converter: KFunction<*>? = converters.getConverter(valueClazz) 42 | 43 | val processor: ParameterProcessor = when { 44 | // converterに一致する組み合わせが有れば設定されていればそれを使う 45 | converter != null -> ParameterProcessor.UseConverter(converter) 46 | // 要求された値がenumかつ元が文字列ならenum mapperでマップ 47 | javaClazz.isEnum && value is String -> ParameterProcessor.ToEnum(javaClazz) 48 | // 要求されているパラメータがStringならtoStringする 49 | clazz == String::class -> ParameterProcessor.ToString 50 | // 入力がmapもしくはpairなら、KMapperを用いてマッピングを試みる 51 | value is Map<*, *> || value is Pair<*, *> -> 52 | ParameterProcessor.UseKMapper(KMapper(clazz, parameterNameConverter)) 53 | else -> ParameterProcessor.UseBoundKMapper(BoundKMapper(clazz, valueClazz, parameterNameConverter)) 54 | } 55 | convertCache.putIfAbsent(valueClazz, processor) 56 | return processor.process(value) 57 | } 58 | } 59 | 60 | private sealed class ParameterProcessor { 61 | abstract fun process(value: Any): Any? 62 | 63 | object Plain : ParameterProcessor() { 64 | override fun process(value: Any): Any = value 65 | } 66 | 67 | class UseConverter(private val converter: KFunction<*>) : ParameterProcessor() { 68 | override fun process(value: Any): Any? = converter.call(value) 69 | } 70 | 71 | class UseKMapper(private val kMapper: KMapper<*>) : ParameterProcessor() { 72 | override fun process(value: Any): Any? = kMapper.map(value, PARAMETER_DUMMY) 73 | } 74 | 75 | @Suppress("UNCHECKED_CAST") 76 | class UseBoundKMapper(private val boundKMapper: BoundKMapper) : ParameterProcessor() { 77 | override fun process(value: Any): Any? = boundKMapper.map(value as T) 78 | } 79 | 80 | class ToEnum(private val javaClazz: Class<*>) : ParameterProcessor() { 81 | override fun process(value: Any): Any? = EnumMapper.getEnum(javaClazz, value as String) 82 | } 83 | 84 | object ToString : ParameterProcessor() { 85 | override fun process(value: Any): Any = value.toString() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KConverter 4 | import com.mapk.conversion.KConvertBy 5 | import com.mapk.core.KFunctionWithInstance 6 | import com.mapk.core.ValueParameter 7 | import com.mapk.core.getAnnotatedFunctions 8 | import com.mapk.core.getAnnotatedFunctionsFromCompanionObject 9 | import com.mapk.core.getKClass 10 | import kotlin.reflect.KClass 11 | import kotlin.reflect.KFunction 12 | import kotlin.reflect.full.findAnnotation 13 | import kotlin.reflect.full.isSubclassOf 14 | import kotlin.reflect.full.primaryConstructor 15 | import kotlin.reflect.full.staticFunctions 16 | import kotlin.reflect.jvm.isAccessible 17 | 18 | internal fun KClass.getConverters(): Set, KFunction>> = 19 | convertersFromConstructors(this) + convertersFromStaticMethods(this) + convertersFromCompanionObject(this) 20 | 21 | private fun Collection>.getConvertersFromFunctions(): Set, KFunction>> { 22 | return this.getAnnotatedFunctions() 23 | .map { func -> 24 | func.isAccessible = true 25 | 26 | func.parameters.single().getKClass() to func 27 | }.toSet() 28 | } 29 | 30 | private fun convertersFromConstructors(clazz: KClass): Set, KFunction>> { 31 | return clazz.constructors.getConvertersFromFunctions() 32 | } 33 | 34 | @Suppress("UNCHECKED_CAST") 35 | private fun convertersFromStaticMethods(clazz: KClass): Set, KFunction>> { 36 | val staticFunctions: Collection> = clazz.staticFunctions as Collection> 37 | 38 | return staticFunctions.getConvertersFromFunctions() 39 | } 40 | 41 | @Suppress("UNCHECKED_CAST") 42 | private fun convertersFromCompanionObject(clazz: KClass): Set, KFunction>> { 43 | return clazz.getAnnotatedFunctionsFromCompanionObject()?.let { (instance, functions) -> 44 | functions.map { function -> 45 | val func: KFunction = KFunctionWithInstance(function, instance) as KFunction 46 | 47 | func.parameters.single().getKClass() to func 48 | }.toSet() 49 | } ?: emptySet() 50 | } 51 | 52 | @Suppress("UNCHECKED_CAST") 53 | internal fun ValueParameter.getConverters(): Set, KFunction<*>>> { 54 | return annotations.mapNotNull { paramAnnotation -> 55 | paramAnnotation.annotationClass 56 | .findAnnotation() 57 | ?.converters 58 | ?.map { it.primaryConstructor!!.call(paramAnnotation) } 59 | }.flatten().map { (it.srcClass) to it::convert as KFunction<*> }.toSet() 60 | } 61 | 62 | // 引数の型がconverterに対して入力可能ならconverterを返す 63 | internal fun Set, KFunction>>.getConverter(input: KClass): KFunction? = 64 | this.find { (key, _) -> input.isSubclassOf(key) }?.second 65 | 66 | // 再帰的マッピング時にKMapperでマップする場合、引数の数が1つだと正常にマッピングが機能しないため、2引数にするために用いるダミー 67 | internal val PARAMETER_DUMMY = "" to null 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KGetterAlias 4 | import com.mapk.annotations.KGetterIgnore 5 | import com.mapk.core.ArgumentAdaptor 6 | import com.mapk.core.KFunctionForCall 7 | import com.mapk.core.toKConstructor 8 | import java.lang.reflect.Method 9 | import kotlin.reflect.KClass 10 | import kotlin.reflect.KFunction 11 | import kotlin.reflect.KVisibility 12 | import kotlin.reflect.full.memberProperties 13 | import kotlin.reflect.jvm.javaGetter 14 | 15 | class PlainKMapper private constructor( 16 | private val function: KFunctionForCall, 17 | parameterNameConverter: ((String) -> String)? 18 | ) { 19 | constructor(function: KFunction, parameterNameConverter: ((String) -> String)? = null) : this( 20 | KFunctionForCall(function, parameterNameConverter), 21 | parameterNameConverter 22 | ) 23 | 24 | constructor(clazz: KClass, parameterNameConverter: ((String) -> String)? = null) : this( 25 | clazz.toKConstructor(parameterNameConverter), 26 | parameterNameConverter 27 | ) 28 | 29 | private val parameterMap: Map> = function.requiredParameters.associate { 30 | it.name to PlainParameterForMap(it, parameterNameConverter) 31 | } 32 | 33 | private fun bindArguments(argumentAdaptor: ArgumentAdaptor, src: Any) { 34 | src::class.memberProperties.forEach outer@{ property -> 35 | // propertyが公開されていない場合は処理を行わない 36 | if (property.visibility != KVisibility.PUBLIC) return@outer 37 | 38 | // ゲッターが取れない場合は処理を行わない 39 | val javaGetter: Method = property.javaGetter ?: return@outer 40 | 41 | var alias: String? = null 42 | // NOTE: IgnoreとAliasが同時に指定されるようなパターンを考慮してaliasが取れてもbreakしていない 43 | javaGetter.annotations.forEach { 44 | if (it is KGetterIgnore) return@outer // ignoreされている場合は処理を行わない 45 | if (it is KGetterAlias) alias = it.value 46 | } 47 | alias = alias ?: property.name 48 | 49 | parameterMap[alias!!]?.let { 50 | // javaGetterを呼び出す方が高速 51 | javaGetter.isAccessible = true 52 | argumentAdaptor.putIfAbsent(alias!!) { javaGetter.invoke(src)?.let { value -> it.mapObject(value) } } 53 | // 終了判定 54 | if (argumentAdaptor.isFullInitialized()) return 55 | } 56 | } 57 | } 58 | 59 | private fun bindArguments(argumentAdaptor: ArgumentAdaptor, src: Map<*, *>) { 60 | src.forEach { (key, value) -> 61 | parameterMap[key]?.let { param -> 62 | // 取得した内容がnullでなければ適切にmapする 63 | argumentAdaptor.putIfAbsent(key as String) { value?.let { param.mapObject(value) } } 64 | // 終了判定 65 | if (argumentAdaptor.isFullInitialized()) return 66 | } 67 | } 68 | } 69 | 70 | private fun bindArguments(argumentBucket: ArgumentAdaptor, srcPair: Pair<*, *>) { 71 | val key = srcPair.first.toString() 72 | 73 | parameterMap[key]?.let { 74 | argumentBucket.putIfAbsent(key) { srcPair.second?.let { value -> it.mapObject(value) } } 75 | } 76 | } 77 | 78 | fun map(srcMap: Map): T { 79 | val adaptor = function.getArgumentAdaptor() 80 | bindArguments(adaptor, srcMap) 81 | 82 | return function.call(adaptor) 83 | } 84 | 85 | fun map(srcPair: Pair): T { 86 | val adaptor = function.getArgumentAdaptor() 87 | bindArguments(adaptor, srcPair) 88 | 89 | return function.call(adaptor) 90 | } 91 | 92 | fun map(src: Any): T { 93 | val adaptor = function.getArgumentAdaptor() 94 | bindArguments(adaptor, src) 95 | 96 | return function.call(adaptor) 97 | } 98 | 99 | fun map(vararg args: Any): T { 100 | val adaptor = function.getArgumentAdaptor() 101 | 102 | listOf(*args).forEach { arg -> 103 | when (arg) { 104 | is Map<*, *> -> bindArguments(adaptor, arg) 105 | is Pair<*, *> -> bindArguments(adaptor, arg) 106 | else -> bindArguments(adaptor, arg) 107 | } 108 | } 109 | 110 | return function.call(adaptor) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.core.EnumMapper 4 | import com.mapk.core.ValueParameter 5 | import kotlin.reflect.KClass 6 | import kotlin.reflect.KFunction 7 | import kotlin.reflect.full.isSuperclassOf 8 | 9 | internal class PlainParameterForMap( 10 | param: ValueParameter, 11 | private val parameterNameConverter: ((String) -> String)? 12 | ) { 13 | private val clazz: KClass = param.requiredClazz 14 | 15 | private val javaClazz: Class by lazy { 16 | clazz.java 17 | } 18 | // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい 19 | @Suppress("UNCHECKED_CAST") 20 | private val converters: Set, KFunction>> by lazy { 21 | (param.getConverters() as Set, KFunction>>) + clazz.getConverters() 22 | } 23 | 24 | fun mapObject(value: U): Any? { 25 | val valueClazz: KClass<*> = value::class 26 | 27 | // パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる 28 | if (clazz.isSuperclassOf(valueClazz)) return value 29 | 30 | val converter: KFunction<*>? = converters.getConverter(valueClazz) 31 | 32 | return when { 33 | // converterに一致する組み合わせが有れば設定されていればそれを使う 34 | converter != null -> converter.call(value) 35 | // 要求された値がenumかつ元が文字列ならenum mapperでマップ 36 | javaClazz.isEnum && value is String -> EnumMapper.getEnum(javaClazz, value) 37 | // 要求されているパラメータがStringならtoStringする 38 | clazz == String::class -> value.toString() 39 | // それ以外の場合PlainKMapperを作り再帰的なマッピングを試みる 40 | else -> PlainKMapper(clazz, parameterNameConverter).map(value, PARAMETER_DUMMY) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/mapk/kmapper/StaticMethodConverter.java: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper; 2 | 3 | import com.mapk.annotations.KConverter; 4 | 5 | import java.util.Arrays; 6 | 7 | public class StaticMethodConverter { 8 | private final int[] arg; 9 | 10 | private StaticMethodConverter(int[] arg) { 11 | this.arg = arg; 12 | } 13 | 14 | public int[] getArg() { 15 | return arg; 16 | } 17 | 18 | @KConverter 19 | private static StaticMethodConverter converter(String csv) { 20 | int[] arg = Arrays.stream(csv.split(",")).mapToInt(Integer::valueOf).toArray(); 21 | return new StaticMethodConverter(arg); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/BoundKMapperInitTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import org.junit.jupiter.api.DisplayName 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.assertDoesNotThrow 6 | import org.junit.jupiter.api.assertThrows 7 | 8 | @Suppress("UNUSED_VARIABLE") 9 | @DisplayName("BoundKMapperの初期化時にエラーを吐かせるテスト") 10 | internal class BoundKMapperInitTest { 11 | data class Dst1(val foo: Int, val bar: Short, val baz: Long) 12 | data class Dst2(val foo: Int, val bar: Short, val baz: Long = 0) 13 | 14 | data class Src(val foo: Int, val bar: Short) 15 | 16 | @Test 17 | @DisplayName("引数が足りない場合のエラーテスト") 18 | fun isError() { 19 | assertThrows { 20 | val mapper: BoundKMapper = BoundKMapper() 21 | } 22 | } 23 | 24 | @Test 25 | @DisplayName("足りないのがオプショナルな引数の場合エラーにならないテスト") 26 | fun isCollect() { 27 | assertDoesNotThrow { val mapper: BoundKMapper = BoundKMapper(::Dst2) } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/BoundParameterForMapTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.kmapper.testcommons.JvmLanguage 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertNull 6 | import org.junit.jupiter.api.DisplayName 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import kotlin.reflect.full.memberProperties 10 | import kotlin.reflect.jvm.javaGetter 11 | 12 | @DisplayName("BoundKMapperのパラメータテスト") 13 | class BoundParameterForMapTest { 14 | data class IntSrc(val int: Int?) 15 | data class StringSrc(val str: String?) 16 | data class InnerSrc(val int: Int?, val str: String?) 17 | data class ObjectSrc(val obj: Any?) 18 | 19 | data class ObjectDst(val int: Int?, val str: String?) 20 | 21 | @Nested 22 | @DisplayName("Plainのテスト") 23 | inner class PlainTest { 24 | private val parameter = 25 | BoundParameterForMap.Plain("", StringSrc::class.memberProperties.single().javaGetter!!) 26 | 27 | @Test 28 | @DisplayName("not null") 29 | fun isNotNull() { 30 | val result = parameter.map(StringSrc("sss")) 31 | assertEquals("sss", result) 32 | } 33 | 34 | @Test 35 | @DisplayName("null") 36 | fun isNull() { 37 | assertNull(parameter.map(StringSrc(null))) 38 | } 39 | } 40 | 41 | @Nested 42 | @DisplayName("UseConverterのテスト") 43 | inner class UseConverterTest { 44 | // アクセシビリティの問題で公開状態に設定 45 | @Suppress("MemberVisibilityCanBePrivate") 46 | fun makeTwiceOrNull(int: Int?) = int?.let { it * 2 } 47 | 48 | private val parameter = BoundParameterForMap.UseConverter( 49 | "", 50 | IntSrc::class.memberProperties.single().javaGetter!!, 51 | this::makeTwiceOrNull 52 | ) 53 | 54 | @Test 55 | @DisplayName("not null") 56 | fun isNotNull() { 57 | val result = parameter.map(IntSrc(1)) 58 | assertEquals(2, result) 59 | } 60 | 61 | @Test 62 | @DisplayName("null") 63 | fun isNull() { 64 | assertNull(parameter.map(IntSrc(null))) 65 | } 66 | } 67 | 68 | @Nested 69 | @DisplayName("UseKMapperのテスト") 70 | inner class UseKMapperTest { 71 | private val parameter = BoundParameterForMap.UseKMapper( 72 | "", 73 | ObjectSrc::class.memberProperties.single().javaGetter!!, 74 | KMapper(::ObjectDst) 75 | ) 76 | 77 | @Test 78 | @DisplayName("not null") 79 | fun isNotNull() { 80 | val result = parameter.map(ObjectSrc(mapOf("int" to 0, "str" to null))) 81 | assertEquals(ObjectDst(0, null), result) 82 | } 83 | 84 | @Test 85 | @DisplayName("null") 86 | fun isNull() { 87 | assertNull(parameter.map(ObjectSrc(null))) 88 | } 89 | } 90 | 91 | @Nested 92 | @DisplayName("UseBoundKMapperのテスト") 93 | inner class UseBoundKMapperTest { 94 | private val parameter = BoundParameterForMap.UseBoundKMapper( 95 | "", 96 | ObjectSrc::class.memberProperties.single().javaGetter!!, 97 | BoundKMapper(::ObjectDst, InnerSrc::class) 98 | ) 99 | 100 | @Test 101 | @DisplayName("not null") 102 | fun isNotNull() { 103 | val result = parameter.map(ObjectSrc(InnerSrc(null, "str"))) 104 | assertEquals(ObjectDst(null, "str"), result) 105 | } 106 | 107 | @Test 108 | @DisplayName("null") 109 | fun isNull() { 110 | assertNull(parameter.map(ObjectSrc(null))) 111 | } 112 | } 113 | 114 | @Nested 115 | @DisplayName("ToEnumのテスト") 116 | inner class ToEnumTest { 117 | private val parameter = BoundParameterForMap.ToEnum( 118 | "", 119 | StringSrc::class.memberProperties.single().javaGetter!!, 120 | JvmLanguage::class.java 121 | ) 122 | 123 | @Test 124 | @DisplayName("not null") 125 | fun isNotNull() { 126 | val result = parameter.map(StringSrc("Java")) 127 | assertEquals(JvmLanguage.Java, result) 128 | } 129 | 130 | @Test 131 | @DisplayName("null") 132 | fun isNull() { 133 | assertNull(parameter.map(StringSrc(null))) 134 | } 135 | } 136 | 137 | @Nested 138 | @DisplayName("ToStringのテスト") 139 | inner class ToStringTest { 140 | private val parameter = 141 | BoundParameterForMap.ToString("", IntSrc::class.memberProperties.single().javaGetter!!) 142 | 143 | @Test 144 | @DisplayName("not null") 145 | fun isNotNull() { 146 | val result = parameter.map(IntSrc(1)) 147 | assertEquals("1", result) 148 | } 149 | 150 | @Test 151 | @DisplayName("null") 152 | fun isNull() { 153 | assertNull(parameter.map(IntSrc(null))) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/ConversionTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.conversion.AbstractKConverter 4 | import com.mapk.conversion.KConvertBy 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Assertions.assertNull 7 | import org.junit.jupiter.api.DisplayName 8 | import org.junit.jupiter.api.Nested 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.assertDoesNotThrow 11 | import org.junit.jupiter.params.ParameterizedTest 12 | import org.junit.jupiter.params.provider.EnumSource 13 | import org.junit.jupiter.params.provider.ValueSource 14 | import java.math.BigDecimal 15 | import java.math.BigInteger 16 | import kotlin.reflect.KClass 17 | import kotlin.reflect.jvm.jvmName 18 | 19 | @DisplayName("KConvertアノテーションによる変換のテスト") 20 | class ConversionTest { 21 | @Target(AnnotationTarget.VALUE_PARAMETER) 22 | @Retention(AnnotationRetention.RUNTIME) 23 | @MustBeDocumented 24 | @KConvertBy([FromString::class, FromNumber::class]) 25 | annotation class ToNumber(val destination: KClass) 26 | 27 | class FromString(annotation: ToNumber) : AbstractKConverter(annotation) { 28 | private val converter: (String) -> Number = when (annotation.destination) { 29 | Double::class -> String::toDouble 30 | Float::class -> String::toFloat 31 | Long::class -> String::toLong 32 | Int::class -> String::toInt 33 | Short::class -> String::toShort 34 | Byte::class -> String::toByte 35 | BigDecimal::class -> { { BigDecimal(it) } } 36 | BigInteger::class -> { { BigInteger(it) } } 37 | else -> throw IllegalArgumentException("${annotation.destination.jvmName} is not supported.") 38 | } 39 | 40 | override val srcClass = String::class 41 | override fun convert(source: String): Number? = source.let(converter) 42 | } 43 | 44 | class FromNumber(annotation: ToNumber) : AbstractKConverter(annotation) { 45 | private val converter: (Number) -> Number = when (annotation.destination) { 46 | Double::class -> Number::toDouble 47 | Float::class -> Number::toFloat 48 | Long::class -> Number::toLong 49 | Int::class -> Number::toInt 50 | Short::class -> Number::toShort 51 | Byte::class -> Number::toByte 52 | BigDecimal::class -> { { BigDecimal.valueOf(it.toDouble()) } } 53 | BigInteger::class -> { { BigInteger.valueOf(it.toLong()) } } 54 | else -> throw IllegalArgumentException("${annotation.destination.jvmName} is not supported.") 55 | } 56 | 57 | override val srcClass = Number::class 58 | override fun convert(source: Number): Number? = source.let(converter) 59 | } 60 | 61 | data class Dst(@ToNumber(BigDecimal::class) val number: BigDecimal?) 62 | data class NumberSrc(val number: Number) 63 | data class StringSrc(val number: String) 64 | object NullSrc { val number: Number? = null } 65 | 66 | enum class NumberSource(val values: Array) { 67 | Doubles(arrayOf(1.0, -2.0, 3.5)), 68 | Floats(arrayOf(4.1f, -5.09f, 6.00001f)), 69 | Longs(arrayOf(7090, 800, 911)), 70 | Ints(arrayOf(0, 123, 234)), 71 | Shorts(arrayOf(365, 416, 511)), 72 | Bytes(arrayOf(6, 7, 8)) 73 | } 74 | 75 | @Nested 76 | @DisplayName("KMapper") 77 | inner class KMapperTest { 78 | private val mapper = KMapper(::Dst) 79 | 80 | @ParameterizedTest 81 | @EnumSource(NumberSource::class) 82 | @DisplayName("Numberソース") 83 | fun fromNumber(numbers: NumberSource) { 84 | numbers.values.forEach { 85 | val actual = mapper.map(NumberSrc(it)) 86 | assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number)) 87 | } 88 | } 89 | 90 | @ParameterizedTest 91 | @ValueSource(strings = ["100", "2.0", "-500"]) 92 | @DisplayName("Stringソース") 93 | fun fromString(str: String) { 94 | val actual = mapper.map(StringSrc(str)) 95 | assertEquals(0, BigDecimal(str).compareTo(actual.number)) 96 | } 97 | 98 | @Test 99 | @DisplayName("nullを入れた際に変換処理に入らないことのテスト") 100 | fun fromNull() { 101 | assertDoesNotThrow { 102 | val actual = mapper.map(NullSrc) 103 | assertNull(actual.number) 104 | } 105 | } 106 | } 107 | 108 | @Nested 109 | @DisplayName("PlainKMapper") 110 | inner class PlainKMapperTest { 111 | private val mapper = PlainKMapper(::Dst) 112 | 113 | @ParameterizedTest 114 | @EnumSource(NumberSource::class) 115 | @DisplayName("Numberソース") 116 | fun fromNumber(numbers: NumberSource) { 117 | numbers.values.forEach { 118 | val actual = mapper.map(NumberSrc(it)) 119 | assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number)) 120 | } 121 | } 122 | 123 | @ParameterizedTest 124 | @ValueSource(strings = ["100", "2.0", "-500"]) 125 | @DisplayName("Stringソース") 126 | fun fromString(str: String) { 127 | val actual = mapper.map(StringSrc(str)) 128 | assertEquals(0, BigDecimal(str).compareTo(actual.number)) 129 | } 130 | 131 | @Test 132 | @DisplayName("nullを入れた際に変換処理に入らないことのテスト") 133 | fun fromNull() { 134 | assertDoesNotThrow { 135 | val actual = mapper.map(NullSrc) 136 | assertNull(actual.number) 137 | } 138 | } 139 | } 140 | 141 | @Nested 142 | @DisplayName("BoundKMapper") 143 | inner class BoundKMapperTest { 144 | @ParameterizedTest 145 | @EnumSource(NumberSource::class) 146 | @DisplayName("Numberソース") 147 | fun fromNumber(numbers: NumberSource) { 148 | numbers.values.forEach { 149 | val actual = BoundKMapper().map(NumberSrc(it)) 150 | assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number)) 151 | } 152 | } 153 | 154 | @ParameterizedTest 155 | @ValueSource(strings = ["100", "2.0", "-500"]) 156 | @DisplayName("Stringソース") 157 | fun fromString(str: String) { 158 | val actual = BoundKMapper().map(StringSrc(str)) 159 | assertEquals(0, BigDecimal(str).compareTo(actual.number)) 160 | } 161 | 162 | @Test 163 | @DisplayName("nullを入れた際に変換処理に入らないことのテスト") 164 | fun fromNull() { 165 | assertDoesNotThrow { 166 | val actual = BoundKMapper(::Dst).map(NullSrc) 167 | assertNull(actual.number) 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.mapk.kmapper 4 | 5 | import com.mapk.annotations.KConverter 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.Assertions.assertTrue 8 | import org.junit.jupiter.api.DisplayName 9 | import org.junit.jupiter.api.Nested 10 | import org.junit.jupiter.api.Test 11 | 12 | private data class ConstructorConverterDst(val argument: ConstructorConverter) 13 | private data class ConstructorConverter @KConverter constructor(val arg: Number) 14 | 15 | private data class CompanionConverterDst(val argument: CompanionConverter) 16 | // NOTE: privateクラスのcompanion objectにアクセスする方法を見つけられなかった 17 | class CompanionConverter private constructor(val arg: String) { 18 | companion object { 19 | @KConverter 20 | private fun converter(arg: String): CompanionConverter { 21 | return CompanionConverter(arg) 22 | } 23 | } 24 | } 25 | 26 | private data class StaticMethodConverterDst(val argument: StaticMethodConverter) 27 | 28 | private data class BoundConstructorConverterSrc(val argument: Int) 29 | private data class BoundCompanionConverterSrc(val argument: String) 30 | private data class BoundStaticMethodConverterSrc(val argument: String) 31 | 32 | @DisplayName("コンバータ有りでのマッピングテスト") 33 | class ConverterKMapperTest { 34 | @Nested 35 | @DisplayName("KMapper") 36 | inner class KMapperTest { 37 | @Test 38 | @DisplayName("コンストラクターでのコンバートテスト") 39 | fun constructorConverterTest() { 40 | val mapper = KMapper(::ConstructorConverterDst) 41 | val result = mapper.map(mapOf("argument" to 1)) 42 | 43 | assertEquals(ConstructorConverter(1), result.argument) 44 | } 45 | 46 | @Test 47 | @DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト") 48 | fun companionConverterTest() { 49 | val mapper = KMapper(CompanionConverterDst::class) 50 | val result = mapper.map(mapOf("argument" to "arg")) 51 | 52 | assertEquals("arg", result.argument.arg) 53 | } 54 | 55 | @Test 56 | @DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト") 57 | fun staticMethodConverterTest() { 58 | val mapper = KMapper() 59 | val result = mapper.map(mapOf("argument" to "1,2,3")) 60 | 61 | assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg) 62 | } 63 | } 64 | 65 | @Nested 66 | @DisplayName("PlainKMapper") 67 | inner class PlainKMapperTest { 68 | @Test 69 | @DisplayName("コンストラクターでのコンバートテスト") 70 | fun constructorConverterTest() { 71 | val mapper = PlainKMapper(ConstructorConverterDst::class) 72 | val result = mapper.map(mapOf("argument" to 1)) 73 | 74 | assertEquals(ConstructorConverter(1), result.argument) 75 | } 76 | 77 | @Test 78 | @DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト") 79 | fun companionConverterTest() { 80 | val mapper = PlainKMapper(::CompanionConverterDst) 81 | val result = mapper.map(mapOf("argument" to "arg")) 82 | 83 | assertEquals("arg", result.argument.arg) 84 | } 85 | 86 | @Test 87 | @DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト") 88 | fun staticMethodConverterTest() { 89 | val mapper = PlainKMapper() 90 | val result = mapper.map(mapOf("argument" to "1,2,3")) 91 | 92 | assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg) 93 | } 94 | } 95 | 96 | @Nested 97 | @DisplayName("BoundKMapper") 98 | inner class BoundKMapperTest { 99 | @Test 100 | @DisplayName("コンストラクターでのコンバートテスト") 101 | fun constructorConverterTest() { 102 | val mapper = BoundKMapper(::ConstructorConverterDst, BoundConstructorConverterSrc::class) 103 | val result = mapper.map(BoundConstructorConverterSrc(1)) 104 | 105 | assertEquals(ConstructorConverter(1), result.argument) 106 | } 107 | 108 | @Test 109 | @DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト") 110 | fun companionConverterTest() { 111 | val mapper: BoundKMapper = BoundKMapper() 112 | val result = mapper.map(BoundCompanionConverterSrc("arg")) 113 | 114 | assertEquals("arg", result.argument.arg) 115 | } 116 | 117 | @Test 118 | @DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト") 119 | fun staticMethodConverterTest() { 120 | val mapper = BoundKMapper(StaticMethodConverterDst::class, BoundStaticMethodConverterSrc::class) 121 | val result = mapper.map(BoundStaticMethodConverterSrc("1,2,3")) 122 | 123 | assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KUseDefaultArgument 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Nested 7 | import org.junit.jupiter.api.Test 8 | 9 | @DisplayName("デフォルト引数を指定するテスト") 10 | class DefaultArgumentTest { 11 | data class Dst(val fooArgument: Int, @param:KUseDefaultArgument val barArgument: String = "default") 12 | data class Src(val fooArgument: Int, val barArgument: String) 13 | 14 | private val src = Src(1, "src") 15 | 16 | @Nested 17 | @DisplayName("KMapper") 18 | inner class KMapperTest { 19 | @Test 20 | fun test() { 21 | val result = KMapper(::Dst).map(src) 22 | assertEquals(Dst(1, "default"), result) 23 | } 24 | } 25 | 26 | @Nested 27 | @DisplayName("PlainKMapper") 28 | inner class PlainKMapperTest { 29 | @Test 30 | fun test() { 31 | val result = PlainKMapper(::Dst).map(src) 32 | assertEquals(Dst(1, "default"), result) 33 | } 34 | } 35 | 36 | @Nested 37 | @DisplayName("BoundKMapper") 38 | inner class BoundKMapperTest { 39 | @Test 40 | fun test() { 41 | val result = BoundKMapper(::Dst, Src::class).map(src) 42 | assertEquals(Dst(1, "default"), result) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.mapk.kmapper 4 | 5 | import com.mapk.kmapper.testcommons.JvmLanguage 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.DisplayName 8 | import org.junit.jupiter.api.Nested 9 | import org.junit.jupiter.params.ParameterizedTest 10 | import org.junit.jupiter.params.provider.EnumSource 11 | 12 | private class EnumMappingDst(val language: JvmLanguage?) 13 | 14 | @DisplayName("文字列 -> Enumのマッピングテスト") 15 | class EnumMappingTest { 16 | @Nested 17 | @DisplayName("KMapper") 18 | inner class KMapperTest { 19 | private val mapper = KMapper(EnumMappingDst::class) 20 | 21 | @ParameterizedTest(name = "Non-Null要求") 22 | @EnumSource(value = JvmLanguage::class) 23 | fun test(language: JvmLanguage) { 24 | val result = mapper.map("language" to language.name) 25 | 26 | assertEquals(language, result.language) 27 | } 28 | } 29 | 30 | @Nested 31 | @DisplayName("PlainKMapper") 32 | inner class PlainKMapperTest { 33 | private val mapper = PlainKMapper(EnumMappingDst::class) 34 | 35 | @ParameterizedTest(name = "Non-Null要求") 36 | @EnumSource(value = JvmLanguage::class) 37 | fun test(language: JvmLanguage) { 38 | val result = mapper.map("language" to language.name) 39 | 40 | assertEquals(language, result.language) 41 | } 42 | } 43 | 44 | data class BoundSrc(val language: String) 45 | 46 | @Nested 47 | @DisplayName("BoundKMapper") 48 | inner class BoundKMapperTest { 49 | private val mapper = BoundKMapper(::EnumMappingDst, BoundSrc::class) 50 | 51 | @ParameterizedTest(name = "Non-Null要求") 52 | @EnumSource(value = JvmLanguage::class) 53 | fun test(language: JvmLanguage) { 54 | val result = mapper.map(BoundSrc(language.name)) 55 | 56 | assertEquals(language, result.language) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KGetterIgnore 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertTrue 6 | import org.junit.jupiter.api.DisplayName 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | 10 | class KGetterIgnoreTest { 11 | data class Src1(val arg1: Int, val arg2: String, @get:KGetterIgnore val arg3: Short) 12 | data class Src2(@get:KGetterIgnore val arg2: String, val arg3: Int, val arg4: String) 13 | 14 | data class Dst(val arg1: Int, val arg2: String, val arg3: Int, val arg4: String) 15 | 16 | @Nested 17 | @DisplayName("KMapper") 18 | inner class KMapperTest { 19 | @Test 20 | @DisplayName("フィールドを無視するテスト") 21 | fun test() { 22 | val src1 = Src1(1, "2-1", 31) 23 | val src2 = Src2("2-2", 32, "4") 24 | 25 | val mapper = KMapper(::Dst) 26 | 27 | val dst1 = mapper.map(src1, src2) 28 | val dst2 = mapper.map(src2, src1) 29 | 30 | assertTrue(dst1 == dst2) 31 | assertEquals(Dst(1, "2-1", 32, "4"), dst1) 32 | } 33 | } 34 | 35 | @Nested 36 | @DisplayName("PlainKMapper") 37 | inner class PlainKMapperTest { 38 | @Test 39 | @DisplayName("フィールドを無視するテスト") 40 | fun test() { 41 | val src1 = Src1(1, "2-1", 31) 42 | val src2 = Src2("2-2", 32, "4") 43 | 44 | val mapper = PlainKMapper(::Dst) 45 | 46 | val dst1 = mapper.map(src1, src2) 47 | val dst2 = mapper.map(src2, src1) 48 | 49 | assertTrue(dst1 == dst2) 50 | assertEquals(Dst(1, "2-1", 32, "4"), dst1) 51 | } 52 | } 53 | 54 | data class BoundSrc(val arg1: Int, val arg2: Short, @get:KGetterIgnore val arg3: String) 55 | data class BoundDst(val arg1: Int, val arg2: Short, val arg3: String = "default") 56 | 57 | @Nested 58 | @DisplayName("BoundKMapper") 59 | inner class BoundKMapperTest { 60 | @Test 61 | @DisplayName("フィールドを無視するテスト") 62 | fun test() { 63 | val mapper = BoundKMapper(::BoundDst, BoundSrc::class) 64 | 65 | val result = mapper.map(BoundSrc(1, 2, "arg3")) 66 | 67 | assertEquals(BoundDst(1, 2, "default"), result) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/KParameterFlattenTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KParameterFlatten 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Test 7 | import java.time.LocalDateTime 8 | 9 | @DisplayName("KParameterFlattenテスト") 10 | class KParameterFlattenTest { 11 | data class InnerDst(val fooFoo: Int, val barBar: String) 12 | data class Dst(@KParameterFlatten val bazBaz: InnerDst, val quxQux: LocalDateTime) 13 | 14 | private val expected = Dst(InnerDst(1, "str"), LocalDateTime.MIN) 15 | 16 | data class Src(val bazBazFooFoo: Int, val bazBazBarBar: String, val quxQux: LocalDateTime, val quuxQuux: Boolean) 17 | private val src = Src(1, "str", LocalDateTime.MIN, false) 18 | 19 | @Test 20 | @DisplayName("BoundKMapper") 21 | fun boundKMapperTest() { 22 | val result = BoundKMapper(::Dst, Src::class).map(src) 23 | assertEquals(expected, result) 24 | } 25 | 26 | @Test 27 | @DisplayName("KMapper") 28 | fun kMapperTest() { 29 | val result = KMapper(::Dst).map(src) 30 | assertEquals(expected, result) 31 | } 32 | 33 | @Test 34 | @DisplayName("PlainKMapper") 35 | fun plainKMapperTest() { 36 | val result = PlainKMapper(::Dst).map(src) 37 | assertEquals(expected, result) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.google.common.base.CaseFormat 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Nested 7 | import org.junit.jupiter.api.Test 8 | 9 | private data class CamelCaseDst(val camelCase: String) 10 | private data class BoundSrc(val camel_case: String) 11 | 12 | @DisplayName("パラメータ名変換のテスト") 13 | class ParameterNameConverterTest { 14 | @Nested 15 | @DisplayName("KMapper") 16 | inner class KMapperTest { 17 | @Test 18 | @DisplayName("スネークケースsrc -> キャメルケースdst") 19 | fun test() { 20 | val expected = "snakeCase" 21 | val src = mapOf("camel_case" to expected) 22 | 23 | val mapper = KMapper { 24 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) 25 | } 26 | val result = mapper.map(src) 27 | 28 | assertEquals(expected, result.camelCase) 29 | } 30 | } 31 | 32 | @Nested 33 | @DisplayName("PlainKMapper") 34 | inner class PlainKMapperTest { 35 | @Test 36 | @DisplayName("スネークケースsrc -> キャメルケースdst") 37 | fun test() { 38 | val expected = "snakeCase" 39 | val src = mapOf("camel_case" to expected) 40 | 41 | val mapper = PlainKMapper { 42 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) 43 | } 44 | val result = mapper.map(src) 45 | 46 | assertEquals(expected, result.camelCase) 47 | } 48 | } 49 | 50 | @Nested 51 | @DisplayName("BoundKMapper") 52 | inner class BoundKMapperTest { 53 | @Test 54 | @DisplayName("スネークケースsrc -> キャメルケースdst") 55 | fun test() { 56 | 57 | val mapper = BoundKMapper { 58 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) 59 | } 60 | val result = mapper.map(BoundSrc("snakeCase")) 61 | 62 | assertEquals(CamelCaseDst("snakeCase"), result) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/PropertyAliasTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.mapk.annotations.KGetterAlias 4 | import com.mapk.annotations.KParameterAlias 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.DisplayName 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | 10 | private data class AliasedDst( 11 | val arg1: Double, 12 | @param:KParameterAlias("arg3") val arg2: Int 13 | ) 14 | 15 | private data class AliasedSrc( 16 | @get:KGetterAlias("arg1") 17 | val arg2: Double, 18 | val arg3: Int 19 | ) 20 | 21 | @DisplayName("エイリアスを貼った場合のテスト") 22 | class PropertyAliasTest { 23 | @Nested 24 | @DisplayName("KMapper") 25 | inner class KMapperTest { 26 | @Test 27 | @DisplayName("パラメータにエイリアスを貼った場合") 28 | fun paramAliasTest() { 29 | val src = mapOf( 30 | "arg1" to 1.0, 31 | "arg2" to "2", 32 | "arg3" to 3 33 | ) 34 | 35 | val result = KMapper(::AliasedDst).map(src) 36 | 37 | assertEquals(1.0, result.arg1) 38 | assertEquals(3, result.arg2) 39 | } 40 | 41 | @Test 42 | @DisplayName("ゲッターにエイリアスを貼った場合") 43 | fun getAliasTest() { 44 | val src = AliasedSrc(1.0, 2) 45 | val result = KMapper(::AliasedDst).map(src) 46 | 47 | assertEquals(1.0, result.arg1) 48 | assertEquals(2, result.arg2) 49 | } 50 | } 51 | 52 | @Nested 53 | @DisplayName("PlainKMapper") 54 | inner class PlainKMapperTest { 55 | @Test 56 | @DisplayName("パラメータにエイリアスを貼った場合") 57 | fun paramAliasTest() { 58 | val src = mapOf( 59 | "arg1" to 1.0, 60 | "arg2" to "2", 61 | "arg3" to 3 62 | ) 63 | 64 | val result = PlainKMapper(::AliasedDst).map(src) 65 | 66 | assertEquals(1.0, result.arg1) 67 | assertEquals(3, result.arg2) 68 | } 69 | 70 | @Test 71 | @DisplayName("ゲッターにエイリアスを貼った場合") 72 | fun getAliasTest() { 73 | val src = AliasedSrc(1.0, 2) 74 | val result = PlainKMapper(::AliasedDst).map(src) 75 | 76 | assertEquals(1.0, result.arg1) 77 | assertEquals(2, result.arg2) 78 | } 79 | } 80 | 81 | @Nested 82 | @DisplayName("BoundKMapper") 83 | inner class BoundKMapperTest { 84 | @Test 85 | @DisplayName("パラメータとゲッターそれぞれエイリアス有りでのマッピング") 86 | fun test() { 87 | val mapper = BoundKMapper(::AliasedDst, AliasedSrc::class) 88 | 89 | val result = mapper.map(AliasedSrc(2.2, 100)) 90 | 91 | assertEquals(AliasedDst(2.2, 100), result) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/RecursiveMappingTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import com.google.common.base.CaseFormat 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Nested 7 | import org.junit.jupiter.api.Test 8 | 9 | @DisplayName("再帰的マッピングのテスト") 10 | class RecursiveMappingTest { 11 | private data class InnerSrc( 12 | val hogeHoge: Int, 13 | val fugaFuga: Short, 14 | val piyoPiyo: String, 15 | val mogeMoge: Pair 16 | ) 17 | private data class InnerSnakeSrc( 18 | val hoge_hoge: Int, 19 | val fuga_fuga: Short, 20 | val piyo_piyo: String, 21 | val moge_moge: Pair 22 | ) 23 | 24 | private data class InnerInnerDst(val poiPoi: Int?) 25 | private data class InnerDst(val hogeHoge: Int, val piyoPiyo: String, val mogeMoge: InnerInnerDst) 26 | 27 | private data class Src(val fooFoo: InnerSrc, val barBar: Boolean, val bazBaz: Int) 28 | private data class SnakeSrc(val foo_foo: InnerSnakeSrc, val bar_bar: Boolean, val baz_baz: Int) 29 | private data class MapSrc(val fooFoo: Map, val barBar: Boolean, val bazBaz: Int) 30 | private data class Dst(val fooFoo: InnerDst, val bazBaz: Int) 31 | 32 | companion object { 33 | private val src = Src(InnerSrc(1, 2, "three", "poiPoi" to 5), true, 4) 34 | private val snakeSrc = SnakeSrc(InnerSnakeSrc(1, 2, "three", "poi_poi" to 5), true, 4) 35 | private val mapSrc = MapSrc(mapOf("hogeHoge" to 1, "piyoPiyo" to "three", "mogeMoge" to ("poiPoi" to 5)), true, 4) 36 | private val expected = Dst(InnerDst(1, "three", InnerInnerDst(5)), 4) 37 | } 38 | 39 | @Nested 40 | @DisplayName("KMapper") 41 | inner class KMapperTest { 42 | @Test 43 | @DisplayName("シンプルなマッピング") 44 | fun test() { 45 | val actual = KMapper(::Dst).map(src) 46 | assertEquals(expected, actual) 47 | } 48 | 49 | @Test 50 | @DisplayName("スネークケースsrc -> キャメルケースdst") 51 | fun snakeToCamel() { 52 | val actual = KMapper(::Dst) { 53 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) 54 | }.map(snakeSrc) 55 | assertEquals(expected, actual) 56 | } 57 | 58 | @Test 59 | @DisplayName("内部フィールドがMapの場合") 60 | fun includesMap() { 61 | val actual = KMapper(::Dst).map(mapSrc) 62 | assertEquals(expected, actual) 63 | } 64 | } 65 | 66 | @Nested 67 | @DisplayName("PlainKMapper") 68 | inner class PlainKMapperTest { 69 | @Test 70 | @DisplayName("シンプルなマッピング") 71 | fun test() { 72 | val actual = PlainKMapper(::Dst).map(src) 73 | assertEquals(expected, actual) 74 | } 75 | 76 | @Test 77 | @DisplayName("スネークケースsrc -> キャメルケースdst") 78 | fun snakeToCamel() { 79 | val actual = PlainKMapper(::Dst) { 80 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) 81 | }.map(snakeSrc) 82 | assertEquals(expected, actual) 83 | } 84 | 85 | @Test 86 | @DisplayName("内部フィールドがMapの場合") 87 | fun includesMap() { 88 | val actual = PlainKMapper(::Dst).map(mapSrc) 89 | assertEquals(expected, actual) 90 | } 91 | } 92 | 93 | @Nested 94 | @DisplayName("BoundKMapper") 95 | inner class BoundKMapperTest { 96 | @Test 97 | @DisplayName("シンプルなマッピング") 98 | fun test() { 99 | val actual = BoundKMapper(::Dst, Src::class).map(src) 100 | assertEquals(expected, actual) 101 | } 102 | 103 | @Test 104 | @DisplayName("スネークケースsrc -> キャメルケースdst") 105 | fun snakeToCamel() { 106 | val actual = BoundKMapper(::Dst, SnakeSrc::class) { 107 | CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) 108 | }.map(snakeSrc) 109 | assertEquals(expected, actual) 110 | } 111 | 112 | @Test 113 | @DisplayName("内部フィールドがMapの場合") 114 | fun includesMap() { 115 | val actual = BoundKMapper(::Dst, MapSrc::class).map(mapSrc) 116 | assertEquals(expected, actual) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/SimpleKMapperTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.mapk.kmapper 4 | 5 | import com.mapk.annotations.KConstructor 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.DisplayName 8 | import org.junit.jupiter.api.Nested 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.TestInstance 11 | import org.junit.jupiter.params.ParameterizedTest 12 | import org.junit.jupiter.params.provider.Arguments 13 | import org.junit.jupiter.params.provider.Arguments.arguments 14 | import org.junit.jupiter.params.provider.MethodSource 15 | import java.math.BigInteger 16 | import java.util.stream.Stream 17 | import kotlin.reflect.full.isSubclassOf 18 | 19 | open class SimpleDst( 20 | val arg1: Int, 21 | val arg2: String?, 22 | val arg3: Number 23 | ) { 24 | companion object { 25 | fun factory(arg1: Int, arg2: String?, arg3: Number): SimpleDst { 26 | return SimpleDst(arg1, arg2, arg3) 27 | } 28 | } 29 | 30 | override fun equals(other: Any?): Boolean { 31 | return other?.takeIf { other::class.isSubclassOf(SimpleDst::class) }?.let { 32 | it as SimpleDst 33 | 34 | return this.arg1 == it.arg1 && this.arg2 == it.arg2 && this.arg3 == it.arg3 35 | } ?: false 36 | } 37 | 38 | override fun hashCode(): Int { 39 | var result = arg1 40 | result = 31 * result + (arg2?.hashCode() ?: 0) 41 | result = 31 * result + arg3.hashCode() 42 | return result 43 | } 44 | } 45 | 46 | class SimpleDstExt( 47 | arg1: Int, 48 | arg2: String?, 49 | arg3: Number 50 | ) : SimpleDst(arg1, arg2, arg3) { 51 | companion object { 52 | @KConstructor 53 | fun factory(arg1: Int, arg2: String?, arg3: Number): SimpleDstExt { 54 | return SimpleDstExt(arg1, arg2, arg3) 55 | } 56 | } 57 | } 58 | 59 | data class Src1( 60 | val arg2: String? 61 | ) { 62 | val arg1: Int = arg2?.length ?: 0 63 | val arg3: Number 64 | get() = arg1.toByte() 65 | val arg4 = null 66 | } 67 | 68 | private data class Src2(val arg2: String?) 69 | 70 | @DisplayName("単純なマッピングのテスト") 71 | class SimpleKMapperTest { 72 | private fun instanceFunction(arg1: Int, arg2: String?, arg3: Number): SimpleDst { 73 | return SimpleDst(arg1, arg2, arg3) 74 | } 75 | 76 | @Nested 77 | @DisplayName("KMapper") 78 | inner class KMapperTest { 79 | private val mappers: Set> = setOf( 80 | KMapper(SimpleDst::class), 81 | KMapper(::SimpleDst), 82 | KMapper((SimpleDst)::factory), 83 | KMapper(::instanceFunction), 84 | KMapper(SimpleDstExt::class) 85 | ) 86 | 87 | @Nested 88 | @DisplayName("Mapからマップ") 89 | inner class FromMap { 90 | @Test 91 | @DisplayName("Nullを含まない場合") 92 | fun testWithoutNull() { 93 | val srcMap: Map = mapOf( 94 | "arg1" to 2, 95 | "arg2" to "value", 96 | "arg3" to 1.0 97 | ) 98 | 99 | val dsts = mappers.map { it.map(srcMap) } 100 | 101 | assertEquals(1, dsts.distinct().size) 102 | dsts.first().let { 103 | assertEquals(2, it.arg1) 104 | assertEquals("value", it.arg2) 105 | assertEquals(1.0, it.arg3) 106 | } 107 | } 108 | 109 | @Test 110 | @DisplayName("Nullを含む場合") 111 | fun testContainsNull() { 112 | val srcMap: Map = mapOf( 113 | "arg1" to 1, 114 | "arg2" to null, 115 | "arg3" to 2.0f 116 | ) 117 | 118 | val dsts = mappers.map { it.map(srcMap) } 119 | 120 | assertEquals(1, dsts.distinct().size) 121 | dsts.first().let { 122 | assertEquals(1, it.arg1) 123 | assertEquals(null, it.arg2) 124 | assertEquals(2.0f, it.arg3) 125 | } 126 | } 127 | } 128 | 129 | @Nested 130 | @DisplayName("インスタンスからマップ") 131 | inner class FromInstance { 132 | @Test 133 | @DisplayName("Nullを含まない場合") 134 | fun testWithoutNull() { 135 | val stringValue = "value" 136 | 137 | val src = Src1(stringValue) 138 | 139 | val dsts = mappers.map { it.map(src) } 140 | 141 | assertEquals(1, dsts.distinct().size) 142 | dsts.first().let { 143 | assertEquals(stringValue.length, it.arg1) 144 | assertEquals(stringValue, it.arg2) 145 | assertEquals(stringValue.length.toByte(), it.arg3) 146 | } 147 | } 148 | 149 | @Test 150 | @DisplayName("Nullを含む場合") 151 | fun testContainsNull() { 152 | val src = Src1(null) 153 | 154 | val dsts = mappers.map { it.map(src) } 155 | 156 | assertEquals(1, dsts.distinct().size) 157 | dsts.first().let { 158 | assertEquals(0, it.arg1) 159 | assertEquals(null, it.arg2) 160 | assertEquals(0.toByte(), it.arg3) 161 | } 162 | } 163 | } 164 | } 165 | 166 | @Nested 167 | @DisplayName("PlainKMapper") 168 | inner class PlainKMapperTest { 169 | private val mappers: Set> = setOf( 170 | PlainKMapper(SimpleDst::class), 171 | PlainKMapper(::SimpleDst), 172 | PlainKMapper((SimpleDst)::factory), 173 | PlainKMapper(::instanceFunction), 174 | PlainKMapper(SimpleDstExt::class) 175 | ) 176 | 177 | @Nested 178 | @DisplayName("Mapからマップ") 179 | inner class FromMap { 180 | @Test 181 | @DisplayName("Nullを含まない場合") 182 | fun testWithoutNull() { 183 | val srcMap: Map = mapOf( 184 | "arg1" to 2, 185 | "arg2" to "value", 186 | "arg3" to 1.0 187 | ) 188 | 189 | val dsts = mappers.map { it.map(srcMap) } 190 | 191 | assertEquals(1, dsts.distinct().size) 192 | dsts.first().let { 193 | assertEquals(2, it.arg1) 194 | assertEquals("value", it.arg2) 195 | assertEquals(1.0, it.arg3) 196 | } 197 | } 198 | 199 | @Test 200 | @DisplayName("Nullを含む場合") 201 | fun testContainsNull() { 202 | val srcMap: Map = mapOf( 203 | "arg1" to 1, 204 | "arg2" to null, 205 | "arg3" to 2.0f 206 | ) 207 | 208 | val dsts = mappers.map { it.map(srcMap) } 209 | 210 | assertEquals(1, dsts.distinct().size) 211 | dsts.first().let { 212 | assertEquals(1, it.arg1) 213 | assertEquals(null, it.arg2) 214 | assertEquals(2.0f, it.arg3) 215 | } 216 | } 217 | } 218 | 219 | @Nested 220 | @DisplayName("インスタンスからマップ") 221 | inner class FromInstance { 222 | @Test 223 | @DisplayName("Nullを含まない場合") 224 | fun testWithoutNull() { 225 | val stringValue = "value" 226 | 227 | val src = Src1(stringValue) 228 | 229 | val dsts = mappers.map { it.map(src) } 230 | 231 | assertEquals(1, dsts.distinct().size) 232 | dsts.first().let { 233 | assertEquals(stringValue.length, it.arg1) 234 | assertEquals(stringValue, it.arg2) 235 | assertEquals(stringValue.length.toByte(), it.arg3) 236 | } 237 | } 238 | 239 | @Test 240 | @DisplayName("Nullを含む場合") 241 | fun testContainsNull() { 242 | val src = Src1(null) 243 | 244 | val dsts = mappers.map { it.map(src) } 245 | 246 | assertEquals(1, dsts.distinct().size) 247 | dsts.first().let { 248 | assertEquals(0, it.arg1) 249 | assertEquals(null, it.arg2) 250 | assertEquals(0.toByte(), it.arg3) 251 | } 252 | } 253 | } 254 | 255 | @Nested 256 | @DisplayName("複数ソースからのマップ") 257 | inner class FromMultipleSrc { 258 | @Test 259 | @DisplayName("Nullを含まない場合") 260 | fun testWithoutNull() { 261 | val src1 = "arg1" to 1 262 | val src2 = Src2("value") 263 | val src3 = mapOf("arg3" to 5.5) 264 | 265 | val dsts = mappers.map { it.map(src1, src2, src3) } 266 | 267 | assertEquals(1, dsts.distinct().size) 268 | dsts.first().let { 269 | assertEquals(1, it.arg1) 270 | assertEquals("value", it.arg2) 271 | assertEquals(5.5, it.arg3) 272 | } 273 | } 274 | 275 | @Test 276 | @DisplayName("Nullを含む場合") 277 | fun testContainsNull() { 278 | val two = BigInteger.valueOf(2L) 279 | 280 | val src1 = "arg1" to 7 281 | val src2 = Src2(null) 282 | val src3 = mapOf("arg3" to two) 283 | 284 | val dsts = mappers.map { it.map(src1, src2, src3) } 285 | 286 | assertEquals(1, dsts.distinct().size) 287 | dsts.first().let { 288 | assertEquals(7, it.arg1) 289 | assertEquals(null, it.arg2) 290 | assertEquals(two, it.arg3) 291 | } 292 | } 293 | } 294 | } 295 | 296 | @Nested 297 | @DisplayName("BoundKMapper") 298 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 299 | inner class BoundKMapperTest { 300 | @Nested 301 | @DisplayName("インスタンスからマップ") 302 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 303 | inner class FromInstance { 304 | fun boundKMapperProvider(): Stream = Stream.of( 305 | arguments("from method reference", BoundKMapper(::SimpleDst, Src1::class)), 306 | arguments("from class", BoundKMapper(SimpleDst::class, Src1::class)) 307 | ) 308 | 309 | @ParameterizedTest(name = "Nullを含まない場合") 310 | @MethodSource("boundKMapperProvider") 311 | fun testWithoutNull( 312 | @Suppress("UNUSED_PARAMETER") name: String, 313 | mapper: BoundKMapper 314 | ) { 315 | val stringValue = "value" 316 | 317 | val src = Src1(stringValue) 318 | 319 | val dst = mapper.map(src) 320 | 321 | assertEquals(stringValue.length, dst.arg1) 322 | assertEquals(stringValue, dst.arg2) 323 | assertEquals(stringValue.length.toByte(), dst.arg3) 324 | } 325 | 326 | @ParameterizedTest(name = "Nullを含む場合") 327 | @MethodSource("boundKMapperProvider") 328 | fun testContainsNull( 329 | @Suppress("UNUSED_PARAMETER") name: String, 330 | mapper: BoundKMapper 331 | ) { 332 | val src = Src1(null) 333 | 334 | val dst = mapper.map(src) 335 | 336 | assertEquals(0, dst.arg1) 337 | assertEquals(null, dst.arg2) 338 | assertEquals(0.toByte(), dst.arg3) 339 | } 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/StringMappingTest.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.DisplayName 5 | import org.junit.jupiter.api.Nested 6 | import org.junit.jupiter.api.Test 7 | 8 | private data class StringMappingDst(val value: String) 9 | private data class BoundMappingSrc(val value: Int) 10 | 11 | @DisplayName("文字列に対してtoStringしたものを渡すテスト") 12 | class StringMappingTest { 13 | @Nested 14 | @DisplayName("KMapper") 15 | inner class KMapperTest { 16 | @Test 17 | fun test() { 18 | val result: StringMappingDst = KMapper(StringMappingDst::class).map("value" to 1) 19 | assertEquals("1", result.value) 20 | } 21 | } 22 | 23 | @Nested 24 | @DisplayName("PlainKMapper") 25 | inner class PlainKMapperTest { 26 | @Test 27 | fun test() { 28 | val result: StringMappingDst = PlainKMapper(StringMappingDst::class).map("value" to 1) 29 | assertEquals("1", result.value) 30 | } 31 | } 32 | 33 | @Nested 34 | @DisplayName("BoundKMapper") 35 | inner class BoundKMapperTest { 36 | @Test 37 | fun test() { 38 | val result = BoundKMapper(::StringMappingDst, BoundMappingSrc::class).map(BoundMappingSrc(100)) 39 | assertEquals("100", result.value) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/kotlin/com/mapk/kmapper/testcommons/JvmLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.mapk.kmapper.testcommons 2 | 3 | enum class JvmLanguage { 4 | Java, Scala, Groovy, Kotlin 5 | } 6 | --------------------------------------------------------------------------------