├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/flexCompiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.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 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/sbt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 | [](https://opensource.org/licenses/Apache-2.0)
2 | [](https://circleci.com/gh/ProjectMapK/KMapper)
3 | [](https://jitci.com/gh/ProjectMapK/KMapper)
4 | [](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 | [](https://opensource.org/licenses/Apache-2.0)
2 | [](https://circleci.com/gh/ProjectMapK/KMapper)
3 | [](https://jitci.com/gh/ProjectMapK/KMapper)
4 | [](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 |
--------------------------------------------------------------------------------