From 2ff7b2b0adb38767a8936704c29926e770b89d1f Mon Sep 17 00:00:00 2001 From: RicardoJiang <2868405029@qq.com> Date: Fri, 24 Nov 2023 15:30:02 +0800 Subject: [PATCH] Kudos add android JsonReader support (#15) * MOD: gitignore * Kudos add android JsonReader support (#13) * ADD: Kudos support JsonReader * ADD: KudosJsonAdapter FIR Support * MOD: Modify naming and version * MOD: jsonReader support Float, Array, Set and Map Type * MOD: module and variable name * MOD: upgrade kotlin-compile-testing-extensions * MOD: Improve empty safety test cases * ADD: jsonReader support null validator * ADD: JsonReader null validator testcase * ADD: Kudos function switch * MOD: throw exception when unkown annotation * Update Version and ReadeMe * Release 1.8.20-1.1.0 (#14) --- .github/scripts/tests.sh | 4 + .gitignore | 3 +- README.md | 48 +- README_zh.md | 51 +- android-json-reader/build.gradle.kts | 22 + android-json-reader/gradle.properties | 1 + .../main/java/android/util/JsonReader.java | 1173 +++++++++++++++++ .../src/main/java/android/util/JsonScope.java | 68 + .../src/main/java/android/util/JsonToken.java | 82 ++ .../android/util/MalformedJsonException.java | 31 + .../com/android/internal/util/StringPool.java | 77 ++ gradle.properties | 4 +- kudos-android-json-reader/build.gradle.kts | 25 + kudos-android-json-reader/gradle.properties | 1 + .../json/reader/KudosAndroidJsonReader.kt | 56 + .../json/reader/adapter/KudosJsonAdapter.kt | 128 ++ .../reader/adapter/ParameterizedTypeImpl.kt | 34 + .../com/kanyun/kudos/annotations/Kudos.kt | 2 +- kudos-compiler/build.gradle.kts | 2 + .../compiler/KudosCompilerPluginRegistrar.kt | 8 +- .../com/kanyun/kudos/compiler/KudosConsts.kt | 64 - .../compiler/KudosFromJsonFunctionBuilder.kt | 303 +++++ .../kudos/compiler/KudosIrClassTransformer.kt | 69 +- .../compiler/KudosIrGenerationExtension.kt | 6 +- .../kudos/compiler/KudosIrTransformer.kt | 3 +- .../com/kanyun/kudos/compiler/KudosNames.kt | 80 ++ .../compiler/k1/KudosDeclarationChecker.kt | 20 +- .../k1/KudosSyntheticResolveExtension.kt | 120 +- .../symbol/FromJsonFunctionDescriptorImpl.kt | 51 + .../kudos/compiler/k2/KudosFirClassChecker.kt | 20 +- .../k2/KudosFirDeclarationGenerator.kt | 141 ++ .../compiler/k2/KudosFirExtensionRegistrar.kt | 10 +- .../KudosFirSupertypeGenerationExtension.kt | 89 +- .../kudos/compiler/k2/KudosPluginKey.kt | 25 + .../kanyun/kudos/compiler/options/Options.kt | 33 + .../kanyun/kudos/compiler/utils/IrClass.kt | 4 +- .../com/kanyun/kudos/compiler/KudosTests.kt | 44 +- .../kanyun/kudos/compiler/base/TestBase.kt | 42 +- .../compiler/generator/TestsGenerator.kt | 29 +- .../{ => common}/classDeclarationCheck.kt | 0 .../testData/{ => common}/constructor.kt | 0 .../testData/{ => common}/defaultValue.kt | 0 .../testData/{ => common}/initBlock.kt | 0 .../testData/{ => common}/notNull.kt | 25 +- .../{ => common}/propertyTypeCheck.kt | 0 .../testData/{ => common}/validator.kt | 2 +- kudos-compiler/testData/gson/annotation.kt | 44 + .../jsonAdapterCheck.kt} | 0 kudos-compiler/testData/gson/notNull.kt | 46 + .../testData/jsonReader/annotation.kt | 100 ++ .../testData/jsonReader/deserialize.kt | 29 + .../jsonReader/deserializeArrayType.kt | 20 + .../jsonReader/deserializeFloatType.kt | 20 + .../testData/jsonReader/deserializeMapType.kt | 38 + .../testData/jsonReader/deserializeSetType.kt | 20 + kudos-compiler/testData/jsonReader/notNull.kt | 24 + kudos-compiler/testData/jsonReader/simple.kt | 99 ++ .../com/kanyun/kudos/gradle/KudosExtension.kt | 1 + .../kanyun/kudos/gradle/KudosGradlePlugin.kt | 7 +- .../java/com/kanyun/kudos/gson/KudosGson.kt | 2 + .../kanyun/kudos/gson/adapter/BoundField.java | 4 +- .../kanyun/kudos/jackson/KudosObjectMapper.kt | 2 + kudos-runtime/build.gradle.kts | 2 +- .../kanyun/kudos/validator/KudosValidator.kt | 2 +- .../kudos-gradle-sample/build.gradle.kts | 3 + kudos-sample/kudos-maven-sample/pom.xml | 10 + settings.gradle.kts | 2 + 67 files changed, 3282 insertions(+), 193 deletions(-) create mode 100644 android-json-reader/build.gradle.kts create mode 100644 android-json-reader/gradle.properties create mode 100644 android-json-reader/src/main/java/android/util/JsonReader.java create mode 100644 android-json-reader/src/main/java/android/util/JsonScope.java create mode 100644 android-json-reader/src/main/java/android/util/JsonToken.java create mode 100644 android-json-reader/src/main/java/android/util/MalformedJsonException.java create mode 100644 android-json-reader/src/main/java/com/android/internal/util/StringPool.java create mode 100644 kudos-android-json-reader/build.gradle.kts create mode 100644 kudos-android-json-reader/gradle.properties create mode 100644 kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt create mode 100644 kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt create mode 100644 kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/ParameterizedTypeImpl.kt delete mode 100644 kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosConsts.kt create mode 100644 kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt create mode 100644 kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt create mode 100644 kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt create mode 100644 kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirDeclarationGenerator.kt create mode 100644 kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosPluginKey.kt rename kudos-compiler/testData/{ => common}/classDeclarationCheck.kt (100%) rename kudos-compiler/testData/{ => common}/constructor.kt (100%) rename kudos-compiler/testData/{ => common}/defaultValue.kt (100%) rename kudos-compiler/testData/{ => common}/initBlock.kt (100%) rename kudos-compiler/testData/{ => common}/notNull.kt (69%) rename kudos-compiler/testData/{ => common}/propertyTypeCheck.kt (100%) rename kudos-compiler/testData/{ => common}/validator.kt (94%) create mode 100644 kudos-compiler/testData/gson/annotation.kt rename kudos-compiler/testData/{gson_jsonAdapterCheck.kt => gson/jsonAdapterCheck.kt} (100%) create mode 100644 kudos-compiler/testData/gson/notNull.kt create mode 100644 kudos-compiler/testData/jsonReader/annotation.kt create mode 100644 kudos-compiler/testData/jsonReader/deserialize.kt create mode 100644 kudos-compiler/testData/jsonReader/deserializeArrayType.kt create mode 100644 kudos-compiler/testData/jsonReader/deserializeFloatType.kt create mode 100644 kudos-compiler/testData/jsonReader/deserializeMapType.kt create mode 100644 kudos-compiler/testData/jsonReader/deserializeSetType.kt create mode 100644 kudos-compiler/testData/jsonReader/notNull.kt create mode 100644 kudos-compiler/testData/jsonReader/simple.kt diff --git a/.github/scripts/tests.sh b/.github/scripts/tests.sh index 2e1fa96..3295a82 100644 --- a/.github/scripts/tests.sh +++ b/.github/scripts/tests.sh @@ -25,4 +25,8 @@ do ./gradlew :kudos-compiler:test -PJACKSON_VERSION=$jackson_version -PVARIANT=jackson -PKOTLIN_COMPILER=K2 done +# Android JsonReader +echo "[Kudos] Testing with Android JsonReader" +./gradlew :kudos-compiler:test -PVARIANT=jsonReader -PKOTLIN_COMPILER=K2 + cd - diff --git a/.gitignore b/.gitignore index 479769a..ec9ffa5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build .idea .gradle *.iml -composite_build.local \ No newline at end of file +composite_build.local +local.properties \ No newline at end of file diff --git a/README.md b/README.md index 55ab2be..dfc3084 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ English | **[简体中文](README_zh.md)** # Kudos -**Kudos** is short for **K**otlin **u**tilities for **d**eserializing **o**bjects. It is designed to make it safer and easier to deserializing Kotlin classes with Gson and Jackson. +**Kudos** is short for **K**otlin **u**tilities for **d**eserializing **o**bjects. It is designed to make it safer and easier to deserializing Kotlin classes with Gson and Jackson. Moreover, it can also simplify the use of the high-performance deserialization framework, JsonReader. ## Background - +### Null Safety and Default Values When parsing JSON using common JSON serialization frameworks, Kotlin developers often face issues with no-arg constructors and property null safety. Let's illustrate these issues with some examples. ```kotlin @@ -99,6 +99,11 @@ Furthermore, Moshi is not always faster than Gson as benchmarks said. Most bench We kept thinking, is there a way to provide null safety and default values support of the primary constructor parameters for frameworks like Gson? The answer is **Kudos**. +### Performance +In actual use, it can be found that powerful frameworks like Gson, Moshi do not have great advantages in performance, while the system's native JsonReader has a clear edge in performance, especially during code cold start stages when it's not fully JIT optimized. However, JsonReader is costly to use, requiring developers to manually parse JSON, which is unfriendly for most developers. + +For this reason, Kudos provides a simple way for developers to use JsonReader through a customized compiler plugin, while ensuring the functionality of type null safety and primary constructor parameter default values. + ## Quick Start ### 1. Introduce Kudos into Gradle Projects @@ -163,6 +168,8 @@ kudos { gson = true // Enable Kudos.Jackson. Add kudos-jackson to dependencies. jackson = true + // Enable Kudos.AndroidJsonReader. Add kudos-android-json-reader to dependencies. + androidJsonReader = true } ```` @@ -323,6 +330,18 @@ println(user) // User(id=12, name=Benny Huo, age=-1, tel=) If the JSON lacks the `id` or `name` field, the parsing fails, ensuring that the properties of `User` is null safe. +The `Kudos` annotation also supports adding parameters like `KUDOS_GSON`, `KUDOS_JACKSON`, `KUDOS_ANDROID_JSON_READER`, to enable support for specific libraries, and also allows passing multiple parameters at the same time, for example: + +```kotlin +@Kudos(KUDOS_GSON, KUDOS_ANDROID_JSON_READER) +data class User( + val id: Long, + val name: String, + val age: Int = -1, + val tel: String = "" +) +``` + ### 4. Collections Properties of collection types declared in classes annotated with @Kudos, such as `List` or `Set`, will be handled carefully in the `validate` function to ensure the null safety for their elements. However, if the type to be parsed is like `List`, Kudos will not be able to provide null safety guarantees at runtime because it cannot obtain whether the element type is nullable. @@ -344,12 +363,25 @@ The code of nullability check has been optimized carefully which is generated by We also find out that Kudos.Gson performs better than Moshi on initializing. So it should be a better choice for low frequency deserialization scenarios with both performance(comparing to Moshi) and null safety(comparing to Gson). -| | small json | medium json | large json | -|---------------|----------------|----------------|----------------| -| Gson | 412,375 ns | 1,374,838 ns | 3,641,904 ns | -| Kudos-Gson | 517,123 ns | 1,686,568 ns | 4,311,910 ns | -| Jackson | 1,035,010 ns | 1,750,709 ns | 3,450,974 ns | -| Kudos-Jackson | 1,261,026 ns | 2,030,874 ns | 3,939,600 ns | +### Multi-run test results +| | small json | medium json | large json | +|------------------|----------------|----------------|----------------| +| Gson | 412,375 ns | 1,374,838 ns | 3,641,904 ns | +| Kudos-Gson | 517,123 ns | 1,686,568 ns | 4,311,910 ns | +| Jackson | 1,035,010 ns | 1,750,709 ns | 3,450,974 ns | +| Kudos-Jackson | 1,261,026 ns | 2,030,874 ns | 3,939,600 ns | +| JsonReader | 190,302 ns | 1,176,479 ns | 3,464,174 ns | +| Kudos-JsonReader | 215,974 ns | 1,359,587 ns | 4,019,024 ns | + +### One-run test results +| | small json | medium json | large json | +|------------------|-----------------|-----------------|-----------------| +| Gson | 3,974,219 ns | 4,666,927 ns | 8,271,355 ns | +| Kudos-Gson | 4,531,718 ns | 6,244,479 ns | 11,160,782 ns | +| Jackson | 12,821,094 ns | 13,930,625 ns | 15,989,791 ns | +| Kudos-Jackson | 13,233,750 ns | 15,674,010 ns | 18,641,302 ns | +| JsonReader | 662,032 ns | 2,056,666 ns | 4,624,687 ns | +| Kudos-JsonReader | 734,907 ns | 2,362,010 ns | 6,212,917 ns | For more detail, see [https://github.com/RicardoJiang/json-benchmark](https://github.com/RicardoJiang/json-benchmark) diff --git a/README_zh.md b/README_zh.md index 6888b59..a0d77b1 100644 --- a/README_zh.md +++ b/README_zh.md @@ -6,10 +6,10 @@ # Kudos -**Kudos** 是 **K**otlin **u**tilities for **d**eserializing **o**bjects 的缩写。它可以解决使用 Gson、Jackson 等框架反序列化 JSON 到 Kotlin 类时所存在的空安全问题和构造器默认值失效的问题。 +**Kudos** 是 **K**otlin **u**tilities for **d**eserializing **o**bjects 的缩写。它可以解决使用 Gson、Jackson 等框架反序列化 JSON 到 Kotlin 类时所存在的空安全问题和构造器默认值失效的问题,同时可以简化高性能的反序列化框架 JsonReader 的使用方式。 ## 问题背景 - +### 空安全与默认值问题 在使用常见的 JSON 序列化框架解析 JSON 时,Kotlin 开发者通常会面临无参构造器和属性空安全的问题。接下来我们通过举例来具体说明这几个问题。 ```kotlin @@ -99,6 +99,11 @@ User(id=12, name=Benny Huo, age=0, tel=null) 我们当时就一直在想,有没有什么办法为 Gson 这样的框架提供类型空安全和支持主构造器的参数默认值的能力呢?答案就是 **Kudos**。 +### 性能问题 +在实际使用中可以发现,Gson, Moshi 等功能强大的框架在性能上并没有很大优势,而系统原生的 JsonReader 在性能上却有着明显的优势,尤其是在冷启动阶段代码未经过充分 JIT 优化时。但是,JsonReader 的使用成本很高,需要开发者自己手动解析 JSON ,这对于大部分开发者来说是不友好的。 + +为此,Kudos 通过自定义编译器插件的方式,为开发者提供了一种简单的方式来使用 JsonReader,同时保证了类型空安全和主构造器参数默认值的功能。 + ## 快速上手 ### 1. 将 Kudos 集成到 Gradle 项目中 @@ -163,6 +168,8 @@ kudos { gson = true // 启用 Kudos.Jackson. 添加 kudos-jackson 依赖. jackson = true + // 启用 Kudos.AndroidJsonReader. 添加 kudos-android-json-reader 依赖. + androidJsonReader = true } ```` @@ -177,6 +184,9 @@ com.kanyun.kudos:kudos-gson // 仅当启用 Kudos.Jackson 时 com.kanyun.kudos:kudos-jackson + +// 仅当启用 Kudos.AndroidJsonReader 时 +com.kanyun.kudos:kudos-android-json-reader ``` 当然,开发者也可以在合适的场景下手动引入这些依赖。 @@ -322,6 +332,18 @@ println(user) // User(id=12, name=Benny Huo, age=-1, tel=) 如果 JSON 中缺少 id 或者 name 字段,则解析失败,确保 User 属性的类型空安全。 +`Kudos`注解也支持添加`KUDOS_GSON`, `KUDOS_JACKSON`, `KUDOS_ANDROID_JSON_READER`等参数, 开启对指定库的支持, 也支持同时传递多个参数,例如: + +```kotlin +@Kudos(KUDOS_GSON, KUDOS_ANDROID_JSON_READER) +data class User( + val id: Long, + val name: String, + val age: Int = -1, + val tel: String = "" +) +``` + ### 4. 集合类型的支持 被 `@Kudos` 标注的类的属性类型如果是集合类型,包括 `List`、`Set` 等,解析之后会在 `validate` 函数中校验元素是否为 `null` 来确保类型空安全。但如果要解析的类型是 `List`,Kudos 在运行时会因为无法获取到元素类型是否可空而无法提供类型空安全的保证。 @@ -343,12 +365,25 @@ val list = kudosGson().fromJson("""[null]""", typeOf>().javaType 在解析 JSON 时,考虑到冷启动的初始化耗时的情况,Kudos.Gson 比 Moshi 在大部分测试下性能更优(只有在多次解析同一数据类型时 Moshi 性能表现更好),因此 Kudos.Gson 在低频次的 JSON 解析场景下兼具了运行性能(优于 Moshi)和数据安全(优于 Gson)的优点。 -| | small json | medium json | large json | -|---------------|----------------|----------------|----------------| -| Gson | 412,375 ns | 1,374,838 ns | 3,641,904 ns | -| Kudos-Gson | 517,123 ns | 1,686,568 ns | 4,311,910 ns | -| Jackson | 1,035,010 ns | 1,750,709 ns | 3,450,974 ns | -| Kudos-Jackson | 1,261,026 ns | 2,030,874 ns | 3,939,600 ns | +### 多次运行测试结果 +| | small json | medium json | large json | +|------------------|----------------|----------------|----------------| +| Gson | 412,375 ns | 1,374,838 ns | 3,641,904 ns | +| Kudos-Gson | 517,123 ns | 1,686,568 ns | 4,311,910 ns | +| Jackson | 1,035,010 ns | 1,750,709 ns | 3,450,974 ns | +| Kudos-Jackson | 1,261,026 ns | 2,030,874 ns | 3,939,600 ns | +| JsonReader | 190,302 ns | 1,176,479 ns | 3,464,174 ns | +| Kudos-JsonReader | 215,974 ns | 1,359,587 ns | 4,019,024 ns | + +### 一次运行测试结果 +| | small json | medium json | large json | +|------------------|-----------------|-----------------|-----------------| +| Gson | 3,974,219 ns | 4,666,927 ns | 8,271,355 ns | +| Kudos-Gson | 4,531,718 ns | 6,244,479 ns | 11,160,782 ns | +| Jackson | 12,821,094 ns | 13,930,625 ns | 15,989,791 ns | +| Kudos-Jackson | 13,233,750 ns | 15,674,010 ns | 18,641,302 ns | +| JsonReader | 662,032 ns | 2,056,666 ns | 4,624,687 ns | +| Kudos-JsonReader | 734,907 ns | 2,362,010 ns | 6,212,917 ns | 更多细节可见:[https://github.com/RicardoJiang/json-benchmark](https://github.com/RicardoJiang/json-benchmark) diff --git a/android-json-reader/build.gradle.kts b/android-json-reader/build.gradle.kts new file mode 100644 index 0000000..2bbcd34 --- /dev/null +++ b/android-json-reader/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + java + kotlin("jvm") +} + +dependencies { +} diff --git a/android-json-reader/gradle.properties b/android-json-reader/gradle.properties new file mode 100644 index 0000000..11bd880 --- /dev/null +++ b/android-json-reader/gradle.properties @@ -0,0 +1 @@ +POM_NAME=android-json-reader diff --git a/android-json-reader/src/main/java/android/util/JsonReader.java b/android-json-reader/src/main/java/android/util/JsonReader.java new file mode 100644 index 0000000..ee0427d --- /dev/null +++ b/android-json-reader/src/main/java/android/util/JsonReader.java @@ -0,0 +1,1173 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import com.android.internal.util.StringPool; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + + +/** + * Reads a JSON (RFC 4627) + * encoded value as a stream of tokens. This stream includes both literal + * values (strings, numbers, booleans, and nulls) as well as the begin and + * end delimiters of objects and arrays. The tokens are traversed in + * depth-first order, the same order that they appear in the JSON document. + * Within JSON objects, name/value pairs are represented by a single token. + * + *

Parsing JSON

+ * To create a recursive descent parser for your own JSON streams, first create + * an entry point method that creates a {@code JsonReader}. + * + *

Next, create handler methods for each structure in your JSON text. You'll + * need a method for each object type and for each array type. + *

    + *
  • Within array handling methods, first call {@link + * #beginArray} to consume the array's opening bracket. Then create a + * while loop that accumulates values, terminating when {@link #hasNext} + * is false. Finally, read the array's closing bracket by calling {@link + * #endArray}. + *
  • Within object handling methods, first call {@link + * #beginObject} to consume the object's opening brace. Then create a + * while loop that assigns values to local variables based on their name. + * This loop should terminate when {@link #hasNext} is false. Finally, + * read the object's closing brace by calling {@link #endObject}. + *
+ *

When a nested object or array is encountered, delegate to the + * corresponding handler method. + * + *

When an unknown name is encountered, strict parsers should fail with an + * exception. Lenient parsers should call {@link #skipValue()} to recursively + * skip the value's nested tokens, which may otherwise conflict. + * + *

If a value may be null, you should first check using {@link #peek()}. + * Null literals can be consumed using either {@link #nextNull()} or {@link + * #skipValue()}. + * + *

Example

+ * Suppose we'd like to parse a stream of messages such as the following:
 {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I read JSON on Android?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "android_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@android_newb just use android.util.JsonReader!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}
+ * This code implements the parser for the above structure:
   {@code
+ *
+ *   public List readJsonStream(InputStream in) throws IOException {
+ *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+ *     try {
+ *       return readMessagesArray(reader);
+ *     } finally {
+ *       reader.close();
+ *     }
+ *   }
+ *
+ *   public List readMessagesArray(JsonReader reader) throws IOException {
+ *     List messages = new ArrayList();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       messages.add(readMessage(reader));
+ *     }
+ *     reader.endArray();
+ *     return messages;
+ *   }
+ *
+ *   public Message readMessage(JsonReader reader) throws IOException {
+ *     long id = -1;
+ *     String text = null;
+ *     User user = null;
+ *     List geo = null;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("id")) {
+ *         id = reader.nextLong();
+ *       } else if (name.equals("text")) {
+ *         text = reader.nextString();
+ *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
+ *         geo = readDoublesArray(reader);
+ *       } else if (name.equals("user")) {
+ *         user = readUser(reader);
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new Message(id, text, user, geo);
+ *   }
+ *
+ *   public List readDoublesArray(JsonReader reader) throws IOException {
+ *     List doubles = new ArrayList();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       doubles.add(reader.nextDouble());
+ *     }
+ *     reader.endArray();
+ *     return doubles;
+ *   }
+ *
+ *   public User readUser(JsonReader reader) throws IOException {
+ *     String username = null;
+ *     int followersCount = -1;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("name")) {
+ *         username = reader.nextString();
+ *       } else if (name.equals("followers_count")) {
+ *         followersCount = reader.nextInt();
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new User(username, followersCount);
+ *   }}
+ * + *

Number Handling

+ * This reader permits numeric values to be read as strings and string values to + * be read as numbers. For example, both elements of the JSON array {@code + * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}. + * This behavior is intended to prevent lossy numeric conversions: double is + * JavaScript's only numeric type and very large values like {@code + * 9007199254740993} cannot be represented exactly on that platform. To minimize + * precision loss, extremely large values should be written and read as strings + * in JSON. + * + *

Each {@code JsonReader} may be used to read a single JSON stream. Instances + * of this class are not thread safe. + */ +public final class JsonReader implements Closeable { + + private static final String TRUE = "true"; + private static final String FALSE = "false"; + + private final StringPool stringPool = new StringPool(); + + /** The input JSON. */ + private final Reader in; + + /** True to accept non-spec compliant JSON */ + private boolean lenient = false; + + /** + * Use a manual buffer to easily read and unread upcoming characters, and + * also so we can create strings without an intermediate StringBuilder. + * We decode literals directly out of this buffer, so it must be at least as + * long as the longest token that can be reported as a number. + */ + private final char[] buffer = new char[1024]; + private int pos = 0; + private int limit = 0; + + /* + * The offset of the first character in the buffer. + */ + private int bufferStartLine = 1; + private int bufferStartColumn = 1; + + private final List stack = new ArrayList(); + { + push(JsonScope.EMPTY_DOCUMENT); + } + + /** + * The type of the next token to be returned by {@link #peek} and {@link + * #advance}. If null, peek() will assign a value. + */ + private JsonToken token; + + /** The text of the next name. */ + private String name; + + /* + * For the next literal value, we may have the text value, or the position + * and length in the buffer. + */ + private String value; + private int valuePos; + private int valueLength; + + /** True if we're currently handling a skipValue() call. */ + private boolean skipping = false; + + /** + * Creates a new instance that reads a JSON-encoded stream from {@code in}. + */ + public JsonReader(Reader in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + this.in = in; + } + + /** + * Configure this parser to be be liberal in what it accepts. By default, + * this parser is strict and only accepts JSON as specified by RFC 4627. Setting the + * parser to lenient causes it to ignore the following syntax errors: + * + *

    + *
  • End of line comments starting with {@code //} or {@code #} and + * ending with a newline character. + *
  • C-style comments starting with {@code /*} and ending with + * {@code *}{@code /}. Such comments may not be nested. + *
  • Names that are unquoted or {@code 'single quoted'}. + *
  • Strings that are unquoted or {@code 'single quoted'}. + *
  • Array elements separated by {@code ;} instead of {@code ,}. + *
  • Unnecessary array separators. These are interpreted as if null + * was the omitted value. + *
  • Names and values separated by {@code =} or {@code =>} instead of + * {@code :}. + *
  • Name/value pairs separated by {@code ;} instead of {@code ,}. + *
+ */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this parser is liberal in what it accepts. + */ + public boolean isLenient() { + return lenient; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new array. + */ + public void beginArray() throws IOException { + expect(JsonToken.BEGIN_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endArray() throws IOException { + expect(JsonToken.END_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new object. + */ + public void beginObject() throws IOException { + expect(JsonToken.BEGIN_OBJECT); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current object. + */ + public void endObject() throws IOException { + expect(JsonToken.END_OBJECT); + } + + /** + * Consumes {@code expected}. + */ + private void expect(JsonToken expected) throws IOException { + peek(); + if (token != expected) { + throw new IllegalStateException("Expected " + expected + " but was " + peek()); + } + advance(); + } + + /** + * Returns true if the current array or object has another element. + */ + public boolean hasNext() throws IOException { + peek(); + return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; + } + + /** + * Returns the type of the next token without consuming it. + */ + public JsonToken peek() throws IOException { + if (token != null) { + return token; + } + + switch (peekStack()) { + case EMPTY_DOCUMENT: + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + JsonToken firstToken = nextValue(); + if (!lenient && token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) { + throw new IOException( + "Expected JSON document to start with '[' or '{' but was " + token); + } + return firstToken; + case EMPTY_ARRAY: + return nextInArray(true); + case NONEMPTY_ARRAY: + return nextInArray(false); + case EMPTY_OBJECT: + return nextInObject(true); + case DANGLING_NAME: + return objectValue(); + case NONEMPTY_OBJECT: + return nextInObject(false); + case NONEMPTY_DOCUMENT: + try { + JsonToken token = nextValue(); + if (lenient) { + return token; + } + throw syntaxError("Expected EOF"); + } catch (EOFException e) { + return token = JsonToken.END_DOCUMENT; // TODO: avoid throwing here? + } + case CLOSED: + throw new IllegalStateException("JsonReader is closed"); + default: + throw new AssertionError(); + } + } + + /** + * Advances the cursor in the JSON stream to the next token. + */ + private JsonToken advance() throws IOException { + peek(); + + JsonToken result = token; + token = null; + value = null; + name = null; + return result; + } + + /** + * Returns the next token, a {@link JsonToken#NAME property name}, and + * consumes it. + * + * @throws IOException if the next token in the stream is not a property + * name. + */ + public String nextName() throws IOException { + peek(); + if (token != JsonToken.NAME) { + throw new IllegalStateException("Expected a name but was " + peek()); + } + String result = name; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#STRING string} value of the next token, + * consuming it. If the next token is a number, this method will return its + * string form. + * + * @throws IllegalStateException if the next token is not a string or if + * this reader is closed. + */ + public String nextString() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected a string but was " + peek()); + } + + String result = value; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, + * consuming it. + * + * @throws IllegalStateException if the next token is not a boolean or if + * this reader is closed. + */ + public boolean nextBoolean() throws IOException { + peek(); + if (token != JsonToken.BOOLEAN) { + throw new IllegalStateException("Expected a boolean but was " + token); + } + + boolean result = (value == TRUE); + advance(); + return result; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is a + * literal null. + * + * @throws IllegalStateException if the next token is not null or if this + * reader is closed. + */ + public void nextNull() throws IOException { + peek(); + if (token != JsonToken.NULL) { + throw new IllegalStateException("Expected null but was " + token); + } + + advance(); + } + + /** + * Returns the {@link JsonToken#NUMBER double} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a double using {@link Double#parseDouble(String)}. + * + * @throws IllegalStateException if the next token is not a literal value. + */ + public double nextDouble() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected a double but was " + token); + } + + double result = Double.parseDouble(value); + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER long} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a long. If the next token's numeric value cannot be exactly + * represented by a Java {@code long}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as a long. + */ + public long nextLong() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected a long but was " + token); + } + + long result; + try { + result = Long.parseLong(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (long) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER int} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as an int. If the next token's numeric value cannot be exactly + * represented by a Java {@code int}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as an int. + */ + public int nextInt() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected an int but was " + token); + } + + int result; + try { + result = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (int) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + advance(); + return result; + } + + /** + * Closes this JSON reader and the underlying {@link Reader}. + */ + public void close() throws IOException { + value = null; + token = null; + stack.clear(); + stack.add(JsonScope.CLOSED); + in.close(); + } + + /** + * Skips the next value recursively. If it is an object or array, all nested + * elements are skipped. This method is intended for use when the JSON token + * stream contains unrecognized or unhandled values. + */ + public void skipValue() throws IOException { + skipping = true; + try { + if (!hasNext() || peek() == JsonToken.END_DOCUMENT) { + throw new IllegalStateException("No element left to skip"); + } + int count = 0; + do { + JsonToken token = advance(); + if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { + count++; + } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { + count--; + } + } while (count != 0); + } finally { + skipping = false; + } + } + + private JsonScope peekStack() { + return stack.get(stack.size() - 1); + } + + private JsonScope pop() { + return stack.remove(stack.size() - 1); + } + + private void push(JsonScope newTop) { + stack.add(newTop); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope newTop) { + stack.set(stack.size() - 1, newTop); + } + + private JsonToken nextInArray(boolean firstElement) throws IOException { + if (firstElement) { + replaceTop(JsonScope.NONEMPTY_ARRAY); + } else { + /* Look for a comma before each element after the first element. */ + switch (nextNonWhitespace()) { + case ']': + pop(); + return token = JsonToken.END_ARRAY; + case ';': + checkLenient(); // fall-through + case ',': + break; + default: + throw syntaxError("Unterminated array"); + } + } + + switch (nextNonWhitespace()) { + case ']': + if (firstElement) { + pop(); + return token = JsonToken.END_ARRAY; + } + // fall-through to handle ",]" + case ';': + case ',': + /* In lenient mode, a 0-length literal means 'null' */ + checkLenient(); + pos--; + value = "null"; + return token = JsonToken.NULL; + default: + pos--; + return nextValue(); + } + } + + private JsonToken nextInObject(boolean firstElement) throws IOException { + /* + * Read delimiters. Either a comma/semicolon separating this and the + * previous name-value pair, or a close brace to denote the end of the + * object. + */ + if (firstElement) { + /* Peek to see if this is the empty object. */ + switch (nextNonWhitespace()) { + case '}': + pop(); + return token = JsonToken.END_OBJECT; + default: + pos--; + } + } else { + switch (nextNonWhitespace()) { + case '}': + pop(); + return token = JsonToken.END_OBJECT; + case ';': + case ',': + break; + default: + throw syntaxError("Unterminated object"); + } + } + + /* Read the name. */ + int quote = nextNonWhitespace(); + switch (quote) { + case '\'': + checkLenient(); // fall-through + case '"': + name = nextString((char) quote); + break; + default: + checkLenient(); + pos--; + name = nextLiteral(false); + if (name.isEmpty()) { + throw syntaxError("Expected name"); + } + } + + replaceTop(JsonScope.DANGLING_NAME); + return token = JsonToken.NAME; + } + + private JsonToken objectValue() throws IOException { + /* + * Read the name/value separator. Usually a colon ':'. In lenient mode + * we also accept an equals sign '=', or an arrow "=>". + */ + switch (nextNonWhitespace()) { + case ':': + break; + case '=': + checkLenient(); + if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { + pos++; + } + break; + default: + throw syntaxError("Expected ':'"); + } + + replaceTop(JsonScope.NONEMPTY_OBJECT); + return nextValue(); + } + + private JsonToken nextValue() throws IOException { + int c = nextNonWhitespace(); + switch (c) { + case '{': + push(JsonScope.EMPTY_OBJECT); + return token = JsonToken.BEGIN_OBJECT; + + case '[': + push(JsonScope.EMPTY_ARRAY); + return token = JsonToken.BEGIN_ARRAY; + + case '\'': + checkLenient(); // fall-through + case '"': + value = nextString((char) c); + return token = JsonToken.STRING; + + default: + pos--; + return readLiteral(); + } + } + + /** + * Returns true once {@code limit - pos >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException { + // Before clobbering the old characters, update where buffer starts + for (int i = 0; i < pos; i++) { + if (buffer[i] == '\n') { + bufferStartLine++; + bufferStartColumn = 1; + } else { + bufferStartColumn++; + } + } + + if (limit != pos) { + limit -= pos; + System.arraycopy(buffer, pos, buffer, 0, limit); + } else { + limit = 0; + } + + pos = 0; + int total; + while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + + // if this is the first read, consume an optional byte order mark (BOM) if it exists + if (bufferStartLine == 1 && bufferStartColumn == 1 + && limit > 0 && buffer[0] == '\ufeff') { + pos++; + bufferStartColumn--; + } + + if (limit >= minimum) { + return true; + } + } + return false; + } + + private int getLineNumber() { + int result = bufferStartLine; + for (int i = 0; i < pos; i++) { + if (buffer[i] == '\n') { + result++; + } + } + return result; + } + + private int getColumnNumber() { + int result = bufferStartColumn; + for (int i = 0; i < pos; i++) { + if (buffer[i] == '\n') { + result = 1; + } else { + result++; + } + } + return result; + } + + private int nextNonWhitespace() throws IOException { + while (pos < limit || fillBuffer(1)) { + int c = buffer[pos++]; + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == limit && !fillBuffer(1)) { + return c; + } + + checkLenient(); + char peek = buffer[pos]; + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + if (!skipTo("*/")) { + throw syntaxError("Unterminated comment"); + } + pos += 2; + continue; + + case '/': + // skip a // end-of-line comment + pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * Skip a # hash end-of-line comment. The JSON RFC doesn't + * specify this behaviour, but it's required to parse + * existing documents. See http://b/2571423. + */ + checkLenient(); + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + throw new EOFException("End of input"); + } + + private void checkLenient() throws IOException { + if (!lenient) { + throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); + } + } + + /** + * Advances the position until after the next newline character. If the line + * is terminated by "\r\n", the '\n' must be consumed as whitespace by the + * caller. + */ + private void skipToEndOfLine() throws IOException { + while (pos < limit || fillBuffer(1)) { + char c = buffer[pos++]; + if (c == '\r' || c == '\n') { + break; + } + } + } + + private boolean skipTo(String toFind) throws IOException { + outer: + for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) { + for (int c = 0; c < toFind.length(); c++) { + if (buffer[pos + c] != toFind.charAt(c)) { + continue outer; + } + } + return true; + } + return false; + } + + /** + * Returns the string up to but not including {@code quote}, unescaping any + * character escape sequences encountered along the way. The opening quote + * should have already been read. This consumes the closing quote, but does + * not include it in the returned string. + * + * @param quote either ' or ". + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private String nextString(char quote) throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + + if (c == quote) { + if (skipping) { + return "skipped!"; + } else if (builder == null) { + return stringPool.get(buffer, start, pos - start - 1); + } else { + builder.append(buffer, start, pos - start - 1); + return builder.toString(); + } + + } else if (c == '\\') { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start - 1); + builder.append(readEscapeCharacter()); + start = pos; + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + throw syntaxError("Unterminated string"); + } + + /** + * Reads the value up to but not including any delimiter characters. This + * does not consume the delimiter character. + * + * @param assignOffsetsOnly true for this method to only set the valuePos + * and valueLength fields and return a null result. This only works if + * the literal is short; a string is returned otherwise. + */ + private String nextLiteral(boolean assignOffsetsOnly) throws IOException { + StringBuilder builder = null; + valuePos = -1; + valueLength = 0; + int i = 0; + + findNonLiteralCharacter: + while (true) { + for (; pos + i < limit; i++) { + switch (buffer[pos + i]) { + case '/': + case '\\': + case ';': + case '#': + case '=': + checkLenient(); // fall-through + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + break findNonLiteralCharacter; + } + } + + /* + * Attempt to load the entire literal into the buffer at once. If + * we run out of input, add a non-literal character at the end so + * that decoding doesn't need to do bounds checks. + */ + if (i < buffer.length) { + if (fillBuffer(i + 1)) { + continue; + } else { + buffer[limit] = '\0'; + break; + } + } + + // use a StringBuilder when the value is too long. It must be an unquoted string. + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, pos, i); + valueLength += i; + pos += i; + i = 0; + if (!fillBuffer(1)) { + break; + } + } + + String result; + if (assignOffsetsOnly && builder == null) { + valuePos = pos; + result = null; + } else if (skipping) { + result = "skipped!"; + } else if (builder == null) { + result = stringPool.get(buffer, pos, i); + } else { + builder.append(buffer, pos, i); + result = builder.toString(); + } + valueLength += i; + pos += i; + return result; + } + + @Override public String toString() { + return getClass().getSimpleName() + " near " + getSnippet(); + } + + /** + * Unescapes the character identified by the character or characters that + * immediately follow a backslash. The backslash '\' should have already + * been read. This supports both unicode escapes "u000A" and two-character + * escapes "\n". + * + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private char readEscapeCharacter() throws IOException { + if (pos == limit && !fillBuffer(1)) { + throw syntaxError("Unterminated escape sequence"); + } + + char escaped = buffer[pos++]; + switch (escaped) { + case 'u': + if (pos + 4 > limit && !fillBuffer(4)) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = stringPool.get(buffer, pos, 4); + pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. + */ + private JsonToken readLiteral() throws IOException { + value = nextLiteral(true); + if (valueLength == 0) { + throw syntaxError("Expected literal value"); + } + token = decodeLiteral(); + if (token == JsonToken.STRING) { + checkLenient(); + } + return token; + } + + /** + * Assigns {@code nextToken} based on the value of {@code nextValue}. + */ + private JsonToken decodeLiteral() throws IOException { + if (valuePos == -1) { + // it was too long to fit in the buffer so it can only be a string + return JsonToken.STRING; + } else if (valueLength == 4 + && ('n' == buffer[valuePos ] || 'N' == buffer[valuePos ]) + && ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1]) + && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2]) + && ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) { + value = "null"; + return JsonToken.NULL; + } else if (valueLength == 4 + && ('t' == buffer[valuePos ] || 'T' == buffer[valuePos ]) + && ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1]) + && ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2]) + && ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) { + value = TRUE; + return JsonToken.BOOLEAN; + } else if (valueLength == 5 + && ('f' == buffer[valuePos ] || 'F' == buffer[valuePos ]) + && ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1]) + && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2]) + && ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3]) + && ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) { + value = FALSE; + return JsonToken.BOOLEAN; + } else { + value = stringPool.get(buffer, valuePos, valueLength); + return decodeNumber(buffer, valuePos, valueLength); + } + } + + /** + * Determine whether the characters is a JSON number. Numbers are of the + * form -12.34e+56. Fractional and exponential parts are optional. Leading + * zeroes are not allowed in the value or exponential part, but are allowed + * in the fraction. + */ + private JsonToken decodeNumber(char[] chars, int offset, int length) { + int i = offset; + int c = chars[i]; + + if (c == '-') { + c = chars[++i]; + } + + if (c == '0') { + c = chars[++i]; + } else if (c >= '1' && c <= '9') { + c = chars[++i]; + while (c >= '0' && c <= '9') { + c = chars[++i]; + } + } else { + return JsonToken.STRING; + } + + if (c == '.') { + c = chars[++i]; + while (c >= '0' && c <= '9') { + c = chars[++i]; + } + } + + if (c == 'e' || c == 'E') { + c = chars[++i]; + if (c == '+' || c == '-') { + c = chars[++i]; + } + if (c >= '0' && c <= '9') { + c = chars[++i]; + while (c >= '0' && c <= '9') { + c = chars[++i]; + } + } else { + return JsonToken.STRING; + } + } + + if (i == offset + length) { + return JsonToken.NUMBER; + } else { + return JsonToken.STRING; + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + private IOException syntaxError(String message) throws IOException { + throw new MalformedJsonException(message + + " at line " + getLineNumber() + " column " + getColumnNumber()); + } + + private CharSequence getSnippet() { + StringBuilder snippet = new StringBuilder(); + int beforePos = Math.min(pos, 20); + snippet.append(buffer, pos - beforePos, beforePos); + int afterPos = Math.min(limit - pos, 20); + snippet.append(buffer, pos, afterPos); + return snippet; + } +} diff --git a/android-json-reader/src/main/java/android/util/JsonScope.java b/android-json-reader/src/main/java/android/util/JsonScope.java new file mode 100644 index 0000000..c622619 --- /dev/null +++ b/android-json-reader/src/main/java/android/util/JsonScope.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * Lexical scoping elements within a JSON reader or writer. + */ +enum JsonScope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + EMPTY_ARRAY, + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no name/value pairs requires no separators or newlines + * before it is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + DANGLING_NAME, + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + NONEMPTY_OBJECT, + + /** + * No object or array has been started. + */ + EMPTY_DOCUMENT, + + /** + * A document with at an array or object. + */ + NONEMPTY_DOCUMENT, + + /** + * A document that's been closed and cannot be accessed. + */ + CLOSED, +} \ No newline at end of file diff --git a/android-json-reader/src/main/java/android/util/JsonToken.java b/android-json-reader/src/main/java/android/util/JsonToken.java new file mode 100644 index 0000000..ef5ba2f --- /dev/null +++ b/android-json-reader/src/main/java/android/util/JsonToken.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * A structure, name or value type in a JSON-encoded string. + */ +public enum JsonToken { + + /** + * The opening of a JSON array. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_ARRAY, + + /** + * The closing of a JSON array. Written using {@link JsonWriter#endArray} + * and read using {@link JsonReader#endArray}. + */ + END_ARRAY, + + /** + * The opening of a JSON object. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_OBJECT, + + /** + * The closing of a JSON object. Written using {@link JsonWriter#endObject} + * and read using {@link JsonReader#endObject}. + */ + END_OBJECT, + + /** + * A JSON property name. Within objects, tokens alternate between names and + * their values. Written using {@link JsonWriter#name} and read using {@link + * JsonReader#nextName} + */ + NAME, + + /** + * A JSON string. + */ + STRING, + + /** + * A JSON number represented in this API by a Java {@code double}, {@code + * long}, or {@code int}. + */ + NUMBER, + + /** + * A JSON {@code true} or {@code false}. + */ + BOOLEAN, + + /** + * A JSON {@code null}. + */ + NULL, + + /** + * The end of the JSON stream. This sentinel value is returned by {@link + * JsonReader#peek()} to signal that the JSON-encoded value has no more + * tokens. + */ + END_DOCUMENT +} diff --git a/android-json-reader/src/main/java/android/util/MalformedJsonException.java b/android-json-reader/src/main/java/android/util/MalformedJsonException.java new file mode 100644 index 0000000..207af09 --- /dev/null +++ b/android-json-reader/src/main/java/android/util/MalformedJsonException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import java.io.IOException; + +/** + * Thrown when a reader encounters malformed JSON. Some syntax errors can be + * ignored by calling {@link JsonReader#setLenient(boolean)}. + */ +public final class MalformedJsonException extends IOException { + private static final long serialVersionUID = 1L; + + public MalformedJsonException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/android-json-reader/src/main/java/com/android/internal/util/StringPool.java b/android-json-reader/src/main/java/com/android/internal/util/StringPool.java new file mode 100644 index 0000000..7d79aac --- /dev/null +++ b/android-json-reader/src/main/java/com/android/internal/util/StringPool.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +/** + * A pool of string instances. Unlike the {@link String#intern() VM's + * interned strings}, this pool provides no guarantee of reference equality. + * It is intended only to save allocations. This class is not thread safe. + * + * @hide + */ +public final class StringPool { + + private final String[] mPool = new String[512]; + + /** + * Constructs string pool. + */ + public StringPool() { + } + + private static boolean contentEquals(String s, char[] chars, int start, int length) { + if (s.length() != length) { + return false; + } + for (int i = 0; i < length; i++) { + if (chars[start + i] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Returns a string equal to {@code new String(array, start, length)}. + * + * @param array buffer containing string chars + * @param start offset in {@code array} where string starts + * @param length length of string + * @return string equal to {@code new String(array, start, length)} + */ + public String get(char[] array, int start, int length) { + // Compute an arbitrary hash of the content + int hashCode = 0; + for (int i = start; i < start + length; i++) { + hashCode = (hashCode * 31) + array[i]; + } + + // Pick a bucket using Doug Lea's supplemental secondaryHash function (from HashMap) + hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12); + hashCode ^= (hashCode >>> 7) ^ (hashCode >>> 4); + int index = hashCode & (mPool.length - 1); + + String pooled = mPool[index]; + if (pooled != null && contentEquals(pooled, array, start, length)) { + return pooled; + } + + String result = new String(array, start, length); + mPool[index] = result; + return result; + } +} diff --git a/gradle.properties b/gradle.properties index 3720d41..cc3f493 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ kotlin.code.style=official -VERSION_NAME=1.9.20-1.0.1 +VERSION_NAME=1.9.20-1.1.0 GROUP=com.kanyun.kudos @@ -30,5 +30,5 @@ RELEASE_SIGNING_ENABLED=true GSON_VERSION=2.10 JACKSON_VERSION=2.15.0 -VARIANT=gson +VARIANT=jsonReader KOTLIN_COMPILER=K2 diff --git a/kudos-android-json-reader/build.gradle.kts b/kudos-android-json-reader/build.gradle.kts new file mode 100644 index 0000000..7f12503 --- /dev/null +++ b/kudos-android-json-reader/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + java + kotlin("jvm") +} + +dependencies { + compileOnly(project(":android-json-reader")) + api(project(":kudos-annotations")) + api(project(":kudos-runtime")) +} diff --git a/kudos-android-json-reader/gradle.properties b/kudos-android-json-reader/gradle.properties new file mode 100644 index 0000000..99e4a5a --- /dev/null +++ b/kudos-android-json-reader/gradle.properties @@ -0,0 +1 @@ +POM_NAME=kudos-android-json-reader diff --git a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt new file mode 100644 index 0000000..4bdd8aa --- /dev/null +++ b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.json.reader + +import android.util.JsonReader +import com.kanyun.kudos.json.reader.adapter.KudosJsonAdapter +import com.kanyun.kudos.json.reader.adapter.parseKudosObject +import java.io.BufferedReader +import java.io.InputStream +import java.lang.reflect.Type + +object KudosAndroidJsonReader { + inline fun fromJson(json: String): T { + return fromJson(json.reader().buffered(), T::class.java) + } + + inline fun fromJson(inputStream: InputStream): T { + return fromJson(inputStream.bufferedReader(), T::class.java) + } + + fun fromJson(bufferedReader: BufferedReader, clazz: Class): T { + val adapter = clazz.getDeclaredConstructor().newInstance() + return if (adapter is KudosJsonAdapter<*>) { + val jsonReader = JsonReader(bufferedReader) + adapter.fromJson(jsonReader) as T + } else { + throw IllegalArgumentException("class ${clazz.name} must implement KudosJsonAdapter") + } + } + + fun fromJson(json: String, type: Type): T { + val jsonReader = JsonReader(json.reader().buffered()) + return parseKudosObject(jsonReader, type) as T + } + + fun fromJson(inputStream: InputStream, type: Type): T { + val jsonReader = JsonReader(inputStream.bufferedReader()) + return parseKudosObject(jsonReader, type) as T + } +} + +const val KUDOS_ANDROID_JSON_READER: Int = 3 diff --git a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt new file mode 100644 index 0000000..2a0abba --- /dev/null +++ b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.json.reader.adapter + +import android.util.JsonReader +import android.util.JsonToken +import com.kanyun.kudos.collections.KudosCollection +import com.kanyun.kudos.collections.KudosList +import com.kanyun.kudos.collections.KudosSet +import java.lang.reflect.Type + +interface KudosJsonAdapter { + fun fromJson(jsonReader: JsonReader): T +} + +fun parseKudosObject(jsonReader: JsonReader, type: Type): Any? { + return if (type is ParameterizedTypeImpl) { + parseKudosObjectInternal(jsonReader, type.rawType, type.actualTypeArguments) + } else { + parseKudosObjectInternal(jsonReader, type, arrayOf()) + } +} + +private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array): List { + val list = mutableListOf() + jsonReader.beginArray() + while (jsonReader.hasNext()) { + list.add(parseKudosObject(jsonReader, typeArguments[0])) + } + jsonReader.endArray() + return list +} + +private fun parseKudosCollection(jsonReader: JsonReader, type: Type, typeArguments: Array): KudosCollection { + val list = KudosList() + jsonReader.beginArray() + while (jsonReader.hasNext()) { + if (jsonReader.peek() == JsonToken.NULL) { + throw NullPointerException("Element cannot be null for ${type.typeName}.") + } + list.add(parseKudosObject(jsonReader, typeArguments[0])!!) + } + jsonReader.endArray() + return list +} + +private fun parseKudosArray(jsonReader: JsonReader, typeArguments: Array): Any { + val list = parseKudosList(jsonReader, typeArguments) + val array = java.lang.reflect.Array.newInstance(typeArguments[0] as Class<*>, list.size) + for (i in list.indices) { + java.lang.reflect.Array.set(array, i, list[i]) + } + return array +} + +private fun parseKudosMap(jsonReader: JsonReader, typeArguments: Array): Map { + val resultMap = mutableMapOf() + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val key = jsonReader.nextName() + val value = parseKudosObject(jsonReader, typeArguments[1]) + resultMap[key] = value + } + jsonReader.endObject() + return resultMap +} + +private fun parseKudosObjectInternal( + jsonReader: JsonReader, + type: Type, + typeArguments: Array, +): Any? { + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + return null + } + val value = when (type) { + String::class.javaObjectType -> jsonReader.nextString() + Int::class.javaObjectType -> jsonReader.nextInt() + Long::class.javaObjectType -> jsonReader.nextLong() + Double::class.javaObjectType -> jsonReader.nextDouble() + Float::class.javaObjectType -> jsonReader.nextString().toFloat() + Boolean::class.javaObjectType -> jsonReader.nextBoolean() + List::class.javaObjectType -> parseKudosList(jsonReader, typeArguments) + Set::class.javaObjectType -> parseKudosList(jsonReader, typeArguments).toSet() + Map::class.javaObjectType -> parseKudosMap(jsonReader, typeArguments) + KudosList::class.javaObjectType -> parseKudosCollection(jsonReader, type, typeArguments) + KudosSet::class.javaObjectType -> parseKudosCollection(jsonReader, type, typeArguments).toCollection(KudosSet()) + KudosCollection::class.javaObjectType -> parseKudosCollection(jsonReader, type, typeArguments) + else -> { + parseKudosObjectSpecial(jsonReader, type, typeArguments) + } + } + return value +} + +private fun parseKudosObjectSpecial( + jsonReader: JsonReader, + type: Type, + typeArguments: Array, +): Any { + return if (type.typeName.endsWith("[]")) { + parseKudosArray(jsonReader, typeArguments) + } else if (type is Class<*>) { + val adapter = type.getDeclaredConstructor().newInstance() + if (adapter is KudosJsonAdapter<*>) { + adapter.fromJson(jsonReader)!! + } else { + throw IllegalArgumentException("class ${type.name} must implement KudosJsonAdapter") + } + } else { + throw IllegalArgumentException("class ${type.typeName} must implement KudosJsonAdapter") + } +} diff --git a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/ParameterizedTypeImpl.kt b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/ParameterizedTypeImpl.kt new file mode 100644 index 0000000..086129a --- /dev/null +++ b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/ParameterizedTypeImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.json.reader.adapter + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ParameterizedTypeImpl(private val type: Type, private val typeArguments: Array) : ParameterizedType { + override fun getActualTypeArguments(): Array { + return typeArguments + } + + override fun getRawType(): Type { + return type + } + + override fun getOwnerType(): Type? { + return null + } +} diff --git a/kudos-annotations/src/main/java/com/kanyun/kudos/annotations/Kudos.kt b/kudos-annotations/src/main/java/com/kanyun/kudos/annotations/Kudos.kt index c6c3874..cdc51bd 100644 --- a/kudos-annotations/src/main/java/com/kanyun/kudos/annotations/Kudos.kt +++ b/kudos-annotations/src/main/java/com/kanyun/kudos/annotations/Kudos.kt @@ -21,4 +21,4 @@ package com.kanyun.kudos.annotations */ @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) -annotation class Kudos +annotation class Kudos(vararg val value: Int) diff --git a/kudos-compiler/build.gradle.kts b/kudos-compiler/build.gradle.kts index 600372e..e2b15ed 100644 --- a/kudos-compiler/build.gradle.kts +++ b/kudos-compiler/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { testImplementation(kotlin("test-junit")) testImplementation(project(":kudos-gson")) testImplementation(project(":kudos-jackson")) + testImplementation(project(":kudos-android-json-reader")) + testImplementation(project(":android-json-reader")) testImplementation("org.jetbrains.kotlin:kotlin-noarg:1.8.20") testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable") testImplementation("com.bennyhuo.kotlin:kotlin-compile-testing-extensions:1.9.20-1.3.0") diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosCompilerPluginRegistrar.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosCompilerPluginRegistrar.kt index f0090db..419dfca 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosCompilerPluginRegistrar.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosCompilerPluginRegistrar.kt @@ -46,11 +46,11 @@ class KudosCompilerPluginRegistrar : CompilerPluginRegistrar() { configuration.get(NoArgConfigurationKeys.PRESET)?.forEach { preset -> NoArgPluginNames.SUPPORTED_PRESETS[preset]?.let { noArgAnnotations += it } } - - IrGenerationExtension.registerExtension(KudosIrGenerationExtension()) - SyntheticResolveExtension.registerExtension(KudosSyntheticResolveExtension()) + val kudosAnnotationValueMap = hashMapOf>() + IrGenerationExtension.registerExtension(KudosIrGenerationExtension(kudosAnnotationValueMap)) + SyntheticResolveExtension.registerExtension(KudosSyntheticResolveExtension(kudosAnnotationValueMap)) StorageComponentContainerContributor.registerExtension(KudosComponentContainerContributor(noArgAnnotations)) - FirExtensionRegistrarAdapter.registerExtension(KudosFirExtensionRegistrar(noArgAnnotations)) + FirExtensionRegistrarAdapter.registerExtension(KudosFirExtensionRegistrar(noArgAnnotations, kudosAnnotationValueMap)) } override val supportsK2: Boolean diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosConsts.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosConsts.kt deleted file mode 100644 index 18a36ff..0000000 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosConsts.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2023 Kanyun, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kanyun.kudos.compiler - -/** - * Created by Benny Huo on 2023/8/21 - */ -const val KUDOS = "com.kanyun.kudos.annotations.Kudos" -const val KUDOS_IGNORE = "com.kanyun.kudos.annotations.KudosIgnore" -const val KUDOS_META = "com.kanyun.kudos.annotations.KudosMeta" -const val KUDOS_VALIDATOR = "com.kanyun.kudos.validator.KudosValidator" -const val VALIDATE_FIELD = "com.kanyun.kudos.validator.validateField" -const val VALIDATE_COLLECTION = "com.kanyun.kudos.validator.validateCollection" -const val VALIDATE_ARRAY = "com.kanyun.kudos.validator.validateArray" - -const val TRANSIENT = "kotlin.jvm.Transient" - -const val ADAPTER_FACTORY = "com.kanyun.kudos.gson.adapter.KudosReflectiveTypeAdapterFactory" - -// Avoid package relocating -val JSON_ADAPTER = "#com.google.gson.annotations.JsonAdapter".removePrefix("#") - -val CONTAINER_FQ_NAMES = setOf( - "kotlin.Array", - - "kotlin.collections.Collection", - "kotlin.collections.MutableCollection", - - "kotlin.collections.List", - "kotlin.collections.MutableList", - "kotlin.collections.ArrayList", - - "kotlin.collections.Set", - "kotlin.collections.MutableSet", - "kotlin.collections.HashSet", - "kotlin.collections.LinkedHashSet", - - "kotlin.collections.Map", - "kotlin.collections.MutableMap", - "kotlin.collections.HashMap", - "kotlin.collections.LinkedHashMap", -) - -fun String.toNonNullContainerType(): String? { - return if (this in CONTAINER_FQ_NAMES) { - "com.kanyun.kudos.runtime.collections.NonNull${removePrefix("kotlin.collections.")}" - } else { - null - } -} diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt new file mode 100644 index 0000000..3779bab --- /dev/null +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.compiler + +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_PEEK_CALLABLE_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_SKIP_VALUE_CALLABLE_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_TOKEN_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_TOKEN_NULL_IDENTIFIER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_JSON_ADAPTER_CLASS_ID +import com.kanyun.kudos.compiler.utils.irThis +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.jvm.ir.kClassReference +import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder +import org.jetbrains.kotlin.ir.builders.Scope +import org.jetbrains.kotlin.ir.builders.irBlock +import org.jetbrains.kotlin.ir.builders.irBranch +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irContinue +import org.jetbrains.kotlin.ir.builders.irElseBranch +import org.jetbrains.kotlin.ir.builders.irEquals +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irGetField +import org.jetbrains.kotlin.ir.builders.irIfThen +import org.jetbrains.kotlin.ir.builders.irNotEquals +import org.jetbrains.kotlin.ir.builders.irNull +import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.builders.irSetField +import org.jetbrains.kotlin.ir.builders.irString +import org.jetbrains.kotlin.ir.builders.irTemporary +import org.jetbrains.kotlin.ir.builders.irVararg +import org.jetbrains.kotlin.ir.builders.irWhen +import org.jetbrains.kotlin.ir.builders.irWhile +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrEnumEntry +import org.jetbrains.kotlin.ir.declarations.IrField +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.IrBranch +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.IrTypeProjection +import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.types.defaultType +import org.jetbrains.kotlin.ir.types.isSubtypeOfClass +import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.util.properties +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +internal class KudosFromJsonFunctionBuilder( + private val irClass: IrClass, + private val irFunction: IrFunction, + private val pluginContext: IrPluginContext, + private val kudosStatusField: IrField?, + private val validatorFunction: IrSimpleFunction?, + startOffset: Int = SYNTHETIC_OFFSET, + endOffset: Int = SYNTHETIC_OFFSET, +) : IrBlockBodyBuilder(pluginContext, Scope(irFunction.symbol), startOffset, endOffset) { + + init { + irFunction.body = doBuild() + } + + private val jsonReader = irFunction.valueParameters.first() + + fun generateBody(): KudosFromJsonFunctionBuilder { + val fields = ArrayList() + irClass.properties.forEach { property -> + if (property.isDelegated) return@forEach + val backingField = property.backingField ?: return@forEach + fields.add(backingField) + } + +irCall( + pluginContext.referenceFunctions( + CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("beginObject")), + ).first(), + ).apply { + dispatchReceiver = irGet(jsonReader) + } + +irWhile().apply { + condition = irCall( + pluginContext.referenceFunctions( + CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("hasNext")), + ).first(), + ).apply { + dispatchReceiver = irGet(jsonReader) + } + body = irBlock { + val name = irTemporary( + irCall( + pluginContext.referenceFunctions( + CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("nextName")), + ).first(), + ).apply { + dispatchReceiver = irGet(jsonReader) + }, + ) + val jsonReaderPeekExpression = irCall(pluginContext.referenceFunctions(JSON_READER_PEEK_CALLABLE_ID).first()).apply { + dispatchReceiver = irGet(jsonReader) + } + val jsonTokenClass = pluginContext.referenceClass(JSON_TOKEN_CLASS_ID)!! + val jsonTokenNullEntry = jsonTokenClass.owner.declarations.filterIsInstance().first { + it.name == JSON_TOKEN_NULL_IDENTIFIER + } + val jsonTokenNullExpression = IrGetEnumValueImpl( + startOffset, + endOffset, + jsonTokenClass.defaultType, + jsonTokenNullEntry.symbol, + ) + +irIfThen( + context.irBuiltIns.unitType, + irEquals(jsonReaderPeekExpression, jsonTokenNullExpression), + irBlock { + +irCall( + pluginContext.referenceFunctions(JSON_READER_SKIP_VALUE_CALLABLE_ID).first(), + ).apply { + dispatchReceiver = irGet(jsonReader) + } + +irContinue(this@apply) + }, + ) + val branches = ArrayList() + fields.forEach { field -> + branches.add( + irBranch( + irEquals(irGet(name), irString(field.name.asString())), + irBlock { + +irSetField(irFunction.irThis(), field, getNextValue(field)) + if (kudosStatusField != null) { + +irCall( + pluginContext.referenceFunctions( + CallableId(FqName("java.util"), FqName("Map"), Name.identifier("put")), + ).first(), + ).apply { + putValueArgument(0, irString(field.name.asString())) + putValueArgument(1, irNotEquals(irGetField(irFunction.irThis(), field), irNull())) + dispatchReceiver = irGetField(irFunction.irThis(), kudosStatusField) + } + } + }, + ), + ) + } + branches.add( + irElseBranch( + irCall(pluginContext.referenceFunctions(JSON_READER_SKIP_VALUE_CALLABLE_ID).first()).apply { + dispatchReceiver = irGet(jsonReader) + }, + ), + ) + +irWhen(context.irBuiltIns.unitType, branches) + } + } + +irCall( + pluginContext.referenceFunctions( + CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("endObject")), + ).first(), + ).apply { + dispatchReceiver = irGet(jsonReader) + } + if (validatorFunction != null && kudosStatusField != null) { + +irCall(validatorFunction.symbol).apply { + putValueArgument(0, irGetField(irFunction.irThis(), kudosStatusField)) + dispatchReceiver = irFunction.irThis() + } + } + +irReturn( + irFunction.irThis(), + ) + return this + } + + private fun getJsonReaderNextSymbol(type: String): IrSimpleFunctionSymbol { + return pluginContext.referenceFunctions( + CallableId( + FqName("android.util"), + FqName("JsonReader"), + Name.identifier("next$type"), + ), + ).first() + } + + private fun getNextValue(field: IrField): IrExpression { + return if (field.type.isSubtypeOfClass(context.irBuiltIns.stringClass)) { + irCall(getJsonReaderNextSymbol("String")).apply { + dispatchReceiver = irGet(jsonReader) + } + } else if (field.type.isSubtypeOfClass(context.irBuiltIns.longClass)) { + irCall(getJsonReaderNextSymbol("Long")).apply { + dispatchReceiver = irGet(jsonReader) + } + } else if (field.type.isSubtypeOfClass(context.irBuiltIns.intClass)) { + irCall(getJsonReaderNextSymbol("Int")).apply { + dispatchReceiver = irGet(jsonReader) + } + } else if (field.type.isSubtypeOfClass(context.irBuiltIns.doubleClass)) { + irCall(getJsonReaderNextSymbol("Double")).apply { + dispatchReceiver = irGet(jsonReader) + } + } else if (field.type.isSubtypeOfClass(context.irBuiltIns.floatClass)) { + irCall( + pluginContext.referenceFunctions( + CallableId(FqName("kotlin.text"), Name.identifier("toFloat")), + ).first().owner, + ).apply { + extensionReceiver = irCall(getJsonReaderNextSymbol("String")).apply { + dispatchReceiver = irGet(jsonReader) + } + } + } else if (field.type.isSubtypeOfClass(context.irBuiltIns.booleanClass)) { + irCall(getJsonReaderNextSymbol("Boolean")).apply { + dispatchReceiver = irGet(jsonReader) + } + } else if ( + field.type.isSubtypeOfClass(context.irBuiltIns.listClass) || + field.type.isSubtypeOfClass(context.irBuiltIns.arrayClass) || + field.type.isSubtypeOfClass(context.irBuiltIns.setClass) || + field.type.isSubtypeOfClass(context.irBuiltIns.mapClass) || + field.type.isSubtypeOfClass( + pluginContext.referenceClass(KUDOS_JSON_ADAPTER_CLASS_ID)!!, + ) + ) { + irCall( + pluginContext.referenceFunctions( + CallableId(FqName("com.kanyun.kudos.json.reader.adapter"), Name.identifier("parseKudosObject")), + ).first(), + ).apply { + putValueArgument(0, irGet(jsonReader)) + putValueArgument(1, getParameterizedType(field.type)) + } + } else { + throw Exception("Kudos UnSupported type ${field.type.classFqName}") + } + } + + private fun getParameterizedType(type: IrType): IrExpression { + var typeArguments = listOf() + if (type is IrSimpleType) { + typeArguments = type.arguments.filterIsInstance() + } + if (typeArguments.isEmpty()) { + return irCall( + pluginContext.referenceProperties( + CallableId(FqName("kotlin.jvm"), Name.identifier("javaObjectType")), + ).first().owner.getter!!, + ).apply { + extensionReceiver = kClassReference(type) + } + } + val irVararg = irVararg( + pluginContext.referenceClass(ClassId(FqName("java.lang.reflect"), Name.identifier("Type")))!!.defaultType, + typeArguments.map { getParameterizedType(it.type) }, + ) + val typeArray = irCall( + pluginContext.referenceFunctions( + CallableId(FqName("kotlin"), Name.identifier("arrayOf")), + ).first(), + ).apply { + putValueArgument(0, irVararg) + } + return irCall( + pluginContext.referenceClass( + ClassId( + FqName("com.kanyun.kudos.json.reader.adapter"), + Name.identifier("ParameterizedTypeImpl"), + ), + )!!.constructors.single(), + ).apply { + putValueArgument( + 0, + irCall( + pluginContext.referenceProperties( + CallableId(FqName("kotlin.jvm"), Name.identifier("javaObjectType")), + ).first().owner.getter!!, + ).apply { + extensionReceiver = kClassReference(type) + }, + ) + putValueArgument(1, typeArray) + } + } +} diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt index b52c04f..dd896d4 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt @@ -16,26 +16,34 @@ package com.kanyun.kudos.compiler +import com.kanyun.kudos.compiler.KudosNames.ADAPTER_FACTORY_NAME +import com.kanyun.kudos.compiler.KudosNames.JSON_ADAPTER_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_FIELD_STATUS_MAP_IDENTIFIER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_VALIDATOR_NAME import com.kanyun.kudos.compiler.options.Options import com.kanyun.kudos.compiler.utils.addOverride import com.kanyun.kudos.compiler.utils.hasKudosAnnotation import com.kanyun.kudos.compiler.utils.irThis import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.Scope +import org.jetbrains.kotlin.ir.builders.declarations.addField import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter import org.jetbrains.kotlin.ir.builders.declarations.buildConstructor import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irExprBody import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.builders.irGetField import org.jetbrains.kotlin.ir.builders.irSetField import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrConstructor import org.jetbrains.kotlin.ir.declarations.IrField +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrGetValue import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl @@ -55,6 +63,7 @@ import org.jetbrains.kotlin.ir.types.starProjectedType import org.jetbrains.kotlin.ir.types.typeOrNull import org.jetbrains.kotlin.ir.types.typeWith import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET +import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.constructors import org.jetbrains.kotlin.ir.util.copyTo import org.jetbrains.kotlin.ir.util.defaultType @@ -78,29 +87,33 @@ class KudosIrClassTransformer( private val context: IrPluginContext, private val irClass: IrClass, private val noArgConstructors: MutableMap, + private val kudosAnnotationValueMap: HashMap>, ) { private val defaults = HashSet() fun transform() { - if (Options.gson()) { + if (Options.isGsonEnabled(kudosAnnotationValueMap, irClass.classId?.asString())) { generateJsonAdapter() } generateNoArgConstructor() - generateValidator() + val validatorFunction = generateValidator() + if (Options.isAndroidJsonReaderEnabled(kudosAnnotationValueMap, irClass.classId?.asString())) { + generateFromJson(validatorFunction) + } } private fun generateJsonAdapter() { - val jsonAdapter = context.referenceConstructors(ClassId.topLevel(FqName(JSON_ADAPTER))).firstOrNull() + val jsonAdapter = context.referenceConstructors(ClassId.topLevel(JSON_ADAPTER_NAME)).firstOrNull() ?: throw IllegalStateException( - "Constructors of class $JSON_ADAPTER not found while isGsonEnabled is set to true. " + + "Constructors of class ${JSON_ADAPTER_NAME.shortName()} not found while isGsonEnabled is set to true. " + "Please check your dependencies to ensure the existing of the Gson library.", ) irClass.annotations += IrConstructorCallImpl.fromSymbolOwner( jsonAdapter.owner.returnType, jsonAdapter.owner.symbol, ).apply { - val adapterFactory = context.referenceClass(ClassId.topLevel(FqName(ADAPTER_FACTORY)))!! + val adapterFactory = context.referenceClass(ClassId.topLevel(ADAPTER_FACTORY_NAME))!! putValueArgument( 0, @@ -199,7 +212,7 @@ class KudosIrClassTransformer( } } - private fun generateValidator() { + private fun generateValidator(): IrSimpleFunction? { val nonDefaults = ArrayList() val collections = ArrayList() val arrays = ArrayList() @@ -229,9 +242,8 @@ class KudosIrClassTransformer( } } - if (nonDefaults.isEmpty() && collections.isEmpty() && arrays.isEmpty()) return + if (nonDefaults.isEmpty() && collections.isEmpty() && arrays.isEmpty()) return null - val kudosValidator = FqName(KUDOS_VALIDATOR) val statusType = context.irBuiltIns.mapClass.typeWith( context.irBuiltIns.stringType, context.irBuiltIns.booleanType, @@ -244,12 +256,12 @@ class KudosIrClassTransformer( } if (validateFunction?.isFakeOverride == false) { - return + return validateFunction } else if (validateFunction?.isFakeOverride == true) { irClass.declarations.remove(validateFunction) } - irClass.addOverride(kudosValidator, "validate", context.irBuiltIns.unitType, Modality.OPEN).apply { + irClass.addOverride(KUDOS_VALIDATOR_NAME, "validate", context.irBuiltIns.unitType, Modality.OPEN).apply { dispatchReceiverParameter = irClass.thisReceiver!!.copyTo(this) val statusParameter = addValueParameter { name = Name.identifier("status") @@ -346,6 +358,7 @@ class KudosIrClassTransformer( } } } + return validateFunction } private fun needsNoargConstructor(declaration: IrClass): Boolean = @@ -359,4 +372,40 @@ class KudosIrClassTransformer( it.defaultValue != null } && (valueParameters.isEmpty() || isPrimary || hasAnnotation(JvmNames.JVM_OVERLOADS_FQ_NAME)) } + + private fun generateFromJson(validatorFunction: IrSimpleFunction?) { + if (irClass.hasKudosAnnotation()) { + val fieldType = context.irBuiltIns.mapClass.typeWith( + context.irBuiltIns.stringClass.defaultType, + context.irBuiltIns.booleanClass.defaultType, + ) + val initExpression = context.referenceFunctions( + CallableId(FqName("kotlin.collections"), Name.identifier("hashMapOf")), + ).first() + val kudosStatusField = if (validatorFunction != null) { + irClass.addField( + KUDOS_FIELD_STATUS_MAP_IDENTIFIER, + fieldType, + ).apply { + initializer = DeclarationIrBuilder( + context, + symbol, + symbol.owner.startOffset, + symbol.owner.endOffset, + ).run { + irExprBody(irCall(initExpression)) + } + } + } else { + null + } + irClass.functions.singleOrNull { + it.name.identifier == "fromJson" + }?.takeIf { + it.body == null + }?.let { function -> + KudosFromJsonFunctionBuilder(irClass, function, context, kudosStatusField, validatorFunction).generateBody() + } + } + } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrGenerationExtension.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrGenerationExtension.kt index 0a8459c..a45b8b1 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrGenerationExtension.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrGenerationExtension.kt @@ -21,9 +21,11 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.ir.visitors.acceptVoid -class KudosIrGenerationExtension : IrGenerationExtension { +class KudosIrGenerationExtension( + private val kudosAnnotationValueMap: HashMap>, +) : IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { - moduleFragment.acceptVoid(KudosIrTransformer(pluginContext)) + moduleFragment.acceptVoid(KudosIrTransformer(pluginContext, kudosAnnotationValueMap)) } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrTransformer.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrTransformer.kt index 20fbcd0..c9d4573 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrTransformer.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrTransformer.kt @@ -29,6 +29,7 @@ import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid */ class KudosIrTransformer( private val context: IrPluginContext, + private val kudosAnnotationValueMap: HashMap>, ) : IrElementVisitorVoid { private val noArgConstructors = mutableMapOf() @@ -41,6 +42,6 @@ class KudosIrTransformer( if (declaration.kind != ClassKind.CLASS) return if (!declaration.hasKudosAnnotation()) return - KudosIrClassTransformer(context, declaration, noArgConstructors).transform() + KudosIrClassTransformer(context, declaration, noArgConstructors, kudosAnnotationValueMap).transform() } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt new file mode 100644 index 0000000..31d9633 --- /dev/null +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.compiler + +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +object KudosNames { + // const + const val KUDOS_VALIDATOR = "com.kanyun.kudos.validator.KudosValidator" + const val KUDOS_JSON_ADAPTER = "com.kanyun.kudos.json.reader.adapter.KudosJsonAdapter" + const val KUDOS_IGNORE = "com.kanyun.kudos.annotations.KudosIgnore" + const val TRANSIENT = "kotlin.jvm.Transient" + + // FqName + val KUDOS_NAME = FqName("com.kanyun.kudos.annotations.Kudos") + val KUDOS_VALIDATOR_NAME = FqName(KUDOS_VALIDATOR) + val KUDOS_IGNORE_NAME = FqName(KUDOS_IGNORE) + val TRANSIENT_NAME = FqName(TRANSIENT) + + // Avoid package relocating + val JSON_ADAPTER_NAME = FqName("#com.google.gson.annotations.JsonAdapter".removePrefix("#")) + val ADAPTER_FACTORY_NAME = FqName("com.kanyun.kudos.gson.adapter.KudosReflectiveTypeAdapterFactory") + + // ClassId + val KUDOS_ANNOTATION_CLASS_ID = ClassId(FqName("com.kanyun.kudos.annotations"), Name.identifier("Kudos")) + val KUDOS_VALIDATOR_CLASS_ID = ClassId(FqName("com.kanyun.kudos.validator"), Name.identifier("KudosValidator")) + val KUDOS_JSON_ADAPTER_CLASS_ID = ClassId(FqName("com.kanyun.kudos.json.reader.adapter"), Name.identifier("KudosJsonAdapter")) + val JSON_READER_CLASS_ID = ClassId.fromString("android/util/JsonReader") + val JSON_TOKEN_CLASS_ID = ClassId(FqName("android.util"), Name.identifier("JsonToken")) + + // CallableId + val JSON_READER_SKIP_VALUE_CALLABLE_ID = CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("skipValue")) + val JSON_READER_PEEK_CALLABLE_ID = CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("peek")) + val JSON_TOKEN_NULL_CALLABLE_ID = CallableId(FqName("android.util"), FqName("JsonToken"), Name.identifier("NULL")) + + // Name.identifier + val KUDOS_VALUE_IDENTIFIER = Name.identifier("value") + val KUDOS_FROM_JSON_IDENTIFIER = Name.identifier("fromJson") + val JSON_READER_IDENTIFIER = Name.identifier("jsonReader") + val KUDOS_FIELD_STATUS_MAP_IDENTIFIER = Name.identifier("kudosFieldStatusMap") + val JSON_TOKEN_NULL_IDENTIFIER = Name.identifier("NULL") + + val CONTAINER_FQ_NAMES = setOf( + "kotlin.Array", + + "kotlin.collections.Collection", + "kotlin.collections.MutableCollection", + + "kotlin.collections.List", + "kotlin.collections.MutableList", + "kotlin.collections.ArrayList", + + "kotlin.collections.Set", + "kotlin.collections.MutableSet", + "kotlin.collections.HashSet", + "kotlin.collections.LinkedHashSet", + + "kotlin.collections.Map", + "kotlin.collections.MutableMap", + "kotlin.collections.HashMap", + "kotlin.collections.LinkedHashMap", + ) +} diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosDeclarationChecker.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosDeclarationChecker.kt index 9243dc5..a7ee0b9 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosDeclarationChecker.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosDeclarationChecker.kt @@ -16,11 +16,11 @@ package com.kanyun.kudos.compiler.k1 -import com.kanyun.kudos.compiler.CONTAINER_FQ_NAMES -import com.kanyun.kudos.compiler.JSON_ADAPTER -import com.kanyun.kudos.compiler.KUDOS -import com.kanyun.kudos.compiler.KUDOS_IGNORE -import com.kanyun.kudos.compiler.TRANSIENT +import com.kanyun.kudos.compiler.KudosNames.CONTAINER_FQ_NAMES +import com.kanyun.kudos.compiler.KudosNames.JSON_ADAPTER_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_IGNORE +import com.kanyun.kudos.compiler.KudosNames.KUDOS_NAME +import com.kanyun.kudos.compiler.KudosNames.TRANSIENT import com.kanyun.kudos.compiler.k1.diagnostic.KudosErrors import com.kanyun.kudos.compiler.k1.utils.initializedWithThisReference import com.kanyun.kudos.compiler.k1.utils.isLazyCall @@ -53,7 +53,7 @@ class KudosDeclarationChecker( if ( declaration is KtClass && descriptor is ClassDescriptor && - descriptor.annotations.hasAnnotation(FqName(KUDOS)) + descriptor.annotations.hasAnnotation(KUDOS_NAME) ) { if (declaration.typeParameters.isNotEmpty()) { context.trace.report(KudosErrors.GENERIC_TYPE.on(declaration)) @@ -94,7 +94,7 @@ class KudosDeclarationChecker( context: DeclarationCheckerContext, declaration: KtDeclaration, ) { - if (descriptor.annotations.hasAnnotation(FqName(JSON_ADAPTER))) { + if (descriptor.annotations.hasAnnotation(JSON_ADAPTER_NAME)) { context.trace.report(KudosErrors.CONFLICTS_WITH_JSON_ADAPTER.on(declaration)) } } @@ -177,7 +177,7 @@ class KudosDeclarationChecker( return emptyList() } - if (type.hasAnnotation(KUDOS)) { + if (type.hasAnnotation(KUDOS_NAME)) { return emptyList() } @@ -192,7 +192,7 @@ class KudosDeclarationChecker( (constructor.declarationDescriptor as? TypeParameterDescriptor)?.representativeUpperBound?.getErasedUpperBound() ?: this - private fun KotlinType.hasAnnotation(fqName: String): Boolean { - return constructor.declarationDescriptor?.annotations?.hasAnnotation(FqName(fqName)) ?: false + private fun KotlinType.hasAnnotation(fqName: FqName): Boolean { + return constructor.declarationDescriptor?.annotations?.hasAnnotation(fqName) ?: false } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosSyntheticResolveExtension.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosSyntheticResolveExtension.kt index e6ba72e..aba1964 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosSyntheticResolveExtension.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/KudosSyntheticResolveExtension.kt @@ -16,48 +16,124 @@ package com.kanyun.kudos.compiler.k1 -import com.kanyun.kudos.compiler.KUDOS +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_IDENTIFIER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_FROM_JSON_IDENTIFIER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_JSON_ADAPTER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_JSON_ADAPTER_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.KUDOS_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_VALIDATOR +import com.kanyun.kudos.compiler.KudosNames.KUDOS_VALIDATOR_CLASS_ID +import com.kanyun.kudos.compiler.k1.symbol.FromJsonFunctionDescriptorImpl +import com.kanyun.kudos.compiler.options.Options +import com.kanyun.kudos.compiler.utils.safeAs import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.descriptors.SourceElement +import org.jetbrains.kotlin.descriptors.annotations.Annotations import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies +import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl import org.jetbrains.kotlin.js.descriptorUtils.getKotlinTypeFqName -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.constants.IntValue +import org.jetbrains.kotlin.resolve.descriptorUtil.classId import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.KotlinTypeFactory +import org.jetbrains.kotlin.types.SimpleType import org.jetbrains.kotlin.types.TypeAttributes +import org.jetbrains.kotlin.types.TypeProjectionImpl import org.jetbrains.kotlin.types.typeUtil.supertypes -class KudosSyntheticResolveExtension : SyntheticResolveExtension { +class KudosSyntheticResolveExtension( + private val kudosAnnotationValueMap: HashMap>, +) : SyntheticResolveExtension { - override fun addSyntheticSupertypes(thisDescriptor: ClassDescriptor, supertypes: MutableList) { + override fun addSyntheticSupertypes( + thisDescriptor: ClassDescriptor, + supertypes: MutableList, + ) { if (thisDescriptor.kind != ClassKind.CLASS) return - if (thisDescriptor.annotations.hasAnnotation(FqName(KUDOS))) { + if (thisDescriptor.annotations.hasAnnotation(KUDOS_NAME)) { + val kudosAnnotation = thisDescriptor.annotations.findAnnotation(KUDOS_NAME) + val annotationValues = kudosAnnotation?.allValueArguments?.values?.firstOrNull()?.value?.safeAs>()?.map { + if (it.value !in Options.validAnnotationList) { + throw IllegalArgumentException("unknown annotation argument ${it.value}") + } + it.value + } + kudosAnnotationValueMap[thisDescriptor.classId?.asString() ?: ""] = annotationValues ?: emptyList() val superTypeNames = supertypes.asSequence().flatMap { listOf(it) + it.supertypes() }.map { it.getKotlinTypeFqName(false) }.toSet() - if ("com.kanyun.kudos.validator.KudosValidator" in superTypeNames) return - - val kudosValidator = thisDescriptor.module.findClassAcrossModuleDependencies( - ClassId( - FqName("com.kanyun.kudos.validator"), - Name.identifier("KudosValidator"), - ), - )!! - - supertypes.add( - KotlinTypeFactory.simpleNotNullType( - TypeAttributes.Empty, - kudosValidator, - emptyList(), - ), - ) + if (KUDOS_VALIDATOR !in superTypeNames) { + val kudosValidator = thisDescriptor.module.findClassAcrossModuleDependencies(KUDOS_VALIDATOR_CLASS_ID)!! + supertypes.add( + KotlinTypeFactory.simpleNotNullType( + TypeAttributes.Empty, + kudosValidator, + emptyList(), + ), + ) + } + if (Options.isAndroidJsonReaderEnabled(kudosAnnotationValueMap, thisDescriptor.classId?.asString())) { + if (KUDOS_JSON_ADAPTER !in superTypeNames) { + val kudosJsonAdapter = thisDescriptor.module.findClassAcrossModuleDependencies(KUDOS_JSON_ADAPTER_CLASS_ID)!! + supertypes.add( + KotlinTypeFactory.simpleNotNullType( + TypeAttributes.Empty, + kudosJsonAdapter, + listOf(TypeProjectionImpl(thisDescriptor.defaultType)), + ), + ) + } + } + } + } + + override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List { + if (Options.isAndroidJsonReaderEnabled(kudosAnnotationValueMap, thisDescriptor.classId?.asString()) && + thisDescriptor.annotations.hasAnnotation(KUDOS_NAME) + ) { + return listOf(KUDOS_FROM_JSON_IDENTIFIER) + } + return super.getSyntheticFunctionNames(thisDescriptor) + } + + override fun generateSyntheticMethods( + thisDescriptor: ClassDescriptor, + name: Name, + bindingContext: BindingContext, + fromSupertypes: List, + result: MutableCollection, + ) { + if (name.identifier == KUDOS_FROM_JSON_IDENTIFIER.identifier) { + if (thisDescriptor.annotations.hasAnnotation(KUDOS_NAME)) { + val jsonReaderType: SimpleType = thisDescriptor.module.findClassAcrossModuleDependencies(JSON_READER_CLASS_ID)?.defaultType ?: return + + result += FromJsonFunctionDescriptorImpl(thisDescriptor).apply { + val valueParameterDescriptor = ValueParameterDescriptorImpl( + containingDeclaration = this, + original = null, + index = 0, + annotations = Annotations.EMPTY, + name = JSON_READER_IDENTIFIER, + outType = jsonReaderType, + declaresDefaultValue = false, + isCrossinline = false, + isNoinline = false, + varargElementType = null, + source = SourceElement.NO_SOURCE, + ) + initialize(listOf(valueParameterDescriptor)) + } + } } } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt new file mode 100644 index 0000000..08ce71b --- /dev/null +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.compiler.k1.symbol + +import com.kanyun.kudos.compiler.KudosNames.KUDOS_FROM_JSON_IDENTIFIER +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl + +class FromJsonFunctionDescriptorImpl( + private val classDescriptor: ClassDescriptor, +) : SimpleFunctionDescriptorImpl( + classDescriptor, + null, + Annotations.EMPTY, + KUDOS_FROM_JSON_IDENTIFIER, + CallableMemberDescriptor.Kind.SYNTHESIZED, + classDescriptor.source, +) { + fun initialize( + valueParameters: List = emptyList(), + ) { + super.initialize( + null, + classDescriptor.thisAsReceiverParameter, + emptyList(), + valueParameters, + classDescriptor.defaultType, + Modality.OPEN, + DescriptorVisibilities.PUBLIC, + ) + } +} diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirClassChecker.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirClassChecker.kt index 7735d19..6da89a7 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirClassChecker.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirClassChecker.kt @@ -16,11 +16,11 @@ package com.kanyun.kudos.compiler.k2 -import com.kanyun.kudos.compiler.CONTAINER_FQ_NAMES -import com.kanyun.kudos.compiler.JSON_ADAPTER -import com.kanyun.kudos.compiler.KUDOS -import com.kanyun.kudos.compiler.KUDOS_IGNORE -import com.kanyun.kudos.compiler.TRANSIENT +import com.kanyun.kudos.compiler.KudosNames.CONTAINER_FQ_NAMES +import com.kanyun.kudos.compiler.KudosNames.JSON_ADAPTER_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_IGNORE_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_NAME +import com.kanyun.kudos.compiler.KudosNames.TRANSIENT_NAME import com.kanyun.kudos.compiler.k2.diagnostic.KudosKtErrors import com.kanyun.kudos.compiler.k2.utils.initializedWithThisReference import com.kanyun.kudos.compiler.k2.utils.isLazyCall @@ -67,7 +67,7 @@ class KudosFirClassChecker( private val noArgAnnotations: List, ) : FirClassChecker() { override fun check(declaration: FirClass, context: CheckerContext, reporter: DiagnosticReporter) { - if (declaration.hasAnnotation(ClassId.topLevel(FqName(KUDOS)), context.session)) { + if (declaration.hasAnnotation(ClassId.topLevel(KUDOS_NAME), context.session)) { if (declaration.typeParameters.isNotEmpty()) { reporter.reportOn(declaration.source, KudosKtErrors.GENERIC_TYPE, context) } else { @@ -110,7 +110,7 @@ class KudosFirClassChecker( context: CheckerContext, reporter: DiagnosticReporter, ) { - if (declaration.hasAnnotation(ClassId.topLevel(FqName(JSON_ADAPTER)), context.session)) { + if (declaration.hasAnnotation(ClassId.topLevel(JSON_ADAPTER_NAME), context.session)) { reporter.reportOn(declaration.source, KudosKtErrors.CONFLICTS_WITH_JSON_ADAPTER, context) } } @@ -173,8 +173,8 @@ class KudosFirClassChecker( reporter: DiagnosticReporter, ) { if (symbol.hasBackingField && !symbol.hasDelegate && - !symbol.hasAnnotation(ClassId.topLevel(FqName(KUDOS_IGNORE)), context.session) && - symbol.backingFieldSymbol?.hasAnnotation(ClassId.topLevel(FqName(TRANSIENT)), context.session) != true + !symbol.hasAnnotation(ClassId.topLevel(KUDOS_IGNORE_NAME), context.session) && + symbol.backingFieldSymbol?.hasAnnotation(ClassId.topLevel(TRANSIENT_NAME), context.session) != true ) { for (type in findNonKudosType(symbol.resolvedReturnType, context)) { reporter.reportOn( @@ -211,7 +211,7 @@ class KudosFirClassChecker( if (annotations != null) { for (annotation in annotations) { val fqName = annotation.fqName(context.session) ?: continue - if (fqName.asString() == KUDOS) { + if (fqName.asString() == KUDOS_NAME.asString()) { return emptyList() } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirDeclarationGenerator.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirDeclarationGenerator.kt new file mode 100644 index 0000000..ace4a3e --- /dev/null +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirDeclarationGenerator.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.compiler.k2 + +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_IDENTIFIER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_FROM_JSON_IDENTIFIER +import com.kanyun.kudos.compiler.KudosNames.KUDOS_NAME +import com.kanyun.kudos.compiler.options.Options +import org.jetbrains.kotlin.GeneratedDeclarationKey +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction +import org.jetbrains.kotlin.fir.declarations.utils.classId +import org.jetbrains.kotlin.fir.declarations.utils.modality +import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension +import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar +import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext +import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate +import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider +import org.jetbrains.kotlin.fir.plugin.SimpleFunctionBuildingContext +import org.jetbrains.kotlin.fir.plugin.createConeType +import org.jetbrains.kotlin.fir.plugin.createMemberFunction +import org.jetbrains.kotlin.fir.scopes.impl.toConeType +import org.jetbrains.kotlin.fir.symbols.SymbolInternals +import org.jetbrains.kotlin.fir.symbols.impl.ConeClassLikeLookupTagImpl +import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol +import org.jetbrains.kotlin.fir.types.ConeKotlinType +import org.jetbrains.kotlin.fir.types.classId +import org.jetbrains.kotlin.fir.types.coneType +import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.utils.addToStdlib.runIf + +class KudosFirDeclarationGenerator( + session: FirSession, + private val kudosAnnotationValueMap: HashMap>, +) : FirDeclarationGenerationExtension(session) { + + companion object { + private val PREDICATE = LookupPredicate.create { + annotated(KUDOS_NAME) + } + private val kudosMethodsNames = setOf(KUDOS_FROM_JSON_IDENTIFIER) + } + + private val matchedClasses by lazy { + session.predicateBasedProvider.getSymbolsByPredicate(PREDICATE) + .filterIsInstance() + } + + private val key: GeneratedDeclarationKey + get() = KudosPluginKey + + override fun FirDeclarationPredicateRegistrar.registerPredicates() { + register(PREDICATE) + } + + override fun getCallableNamesForClass(classSymbol: FirClassSymbol<*>, context: MemberGenerationContext): Set { + if (Options.isAndroidJsonReaderEnabled(kudosAnnotationValueMap, classSymbol.classId.toString())) { + if (classSymbol in matchedClasses) { + return kudosMethodsNames + } + } + return super.getCallableNamesForClass(classSymbol, context) + } + + @OptIn(SymbolInternals::class) + override fun generateFunctions( + callableId: CallableId, + context: MemberGenerationContext?, + ): List { + val owner = context?.owner ?: return emptyList() + require(owner is FirRegularClassSymbol) + if (callableId.callableName == KUDOS_FROM_JSON_IDENTIFIER) { + val declaredFunctions = + owner.declarationSymbols.filterIsInstance() + val function = runIf(declaredFunctions.none { it.isFromJson() }) { + val thisTypeFir = owner.fir + val returnType = ConeClassLikeTypeImpl( + ConeClassLikeLookupTagImpl(thisTypeFir.classId), + thisTypeFir.typeParameters.map { + it.toConeType() + }.toTypedArray(), + false, + ) + createMemberFunctionForKudos( + owner, + callableId.callableName, + returnType, + ) { + valueParameter( + JSON_READER_IDENTIFIER, + JSON_READER_CLASS_ID.createConeType(session), + ) + } + } + if (function != null) return listOf(function.symbol) + } + return super.generateFunctions(callableId, context) + } + + private inline fun createMemberFunctionForKudos( + owner: FirRegularClassSymbol, + name: Name, + returnType: ConeKotlinType, + crossinline init: SimpleFunctionBuildingContext.() -> Unit = {}, + ): FirSimpleFunction { + return createMemberFunction(owner, key, name, returnType) { + modality = if (owner.modality == Modality.FINAL) Modality.FINAL else Modality.OPEN + status { isOverride = true } + init() + } + } + + private fun FirNamedFunctionSymbol.isFromJson(): Boolean { + if (name != KUDOS_FROM_JSON_IDENTIFIER) return false + val parameterSymbols = valueParameterSymbols + if (parameterSymbols.size != 1) return false + val jsonReaderSymbol = parameterSymbols[0] + if (jsonReaderSymbol.resolvedReturnTypeRef.coneType.classId != JSON_READER_CLASS_ID) return false + return true + } +} diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirExtensionRegistrar.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirExtensionRegistrar.kt index 3b52b04..dedfe74 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirExtensionRegistrar.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirExtensionRegistrar.kt @@ -17,18 +17,26 @@ package com.kanyun.kudos.compiler.k2 import org.jetbrains.kotlin.fir.analysis.extensions.FirAdditionalCheckersExtension +import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar +import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension /** * Created by Benny Huo on 2023/8/21 */ class KudosFirExtensionRegistrar( private val noArgAnnotations: List, + private val kudosAnnotationValueMap: HashMap>, ) : FirExtensionRegistrar() { override fun ExtensionRegistrarContext.configurePlugin() { +FirAdditionalCheckersExtension.Factory { session -> KudosFirCheckers(session, noArgAnnotations) } - +::KudosFirSupertypeGenerationExtension + +FirSupertypeGenerationExtension.Factory { session -> + KudosFirSupertypeGenerationExtension(session, kudosAnnotationValueMap) + } + +FirDeclarationGenerationExtension.Factory { session -> + KudosFirDeclarationGenerator(session, kudosAnnotationValueMap) + } } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirSupertypeGenerationExtension.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirSupertypeGenerationExtension.kt index b4eef25..076aca5 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirSupertypeGenerationExtension.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosFirSupertypeGenerationExtension.kt @@ -16,65 +16,107 @@ package com.kanyun.kudos.compiler.k2 -import com.kanyun.kudos.compiler.KUDOS +import com.kanyun.kudos.compiler.KudosNames.KUDOS_ANNOTATION_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.KUDOS_JSON_ADAPTER_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.KUDOS_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_VALIDATOR_CLASS_ID +import com.kanyun.kudos.compiler.options.Options +import com.kanyun.kudos.compiler.utils.safeAs import org.jetbrains.kotlin.fir.FirSession import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin import org.jetbrains.kotlin.fir.declarations.FirRegularClass import org.jetbrains.kotlin.fir.declarations.FirTypeAlias +import org.jetbrains.kotlin.fir.declarations.getAnnotationByClassId import org.jetbrains.kotlin.fir.declarations.utils.classId +import org.jetbrains.kotlin.fir.expressions.FirAnnotation +import org.jetbrains.kotlin.fir.expressions.FirAnnotationCall +import org.jetbrains.kotlin.fir.expressions.FirPropertyAccessExpression +import org.jetbrains.kotlin.fir.expressions.arguments import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension import org.jetbrains.kotlin.fir.extensions.predicate.DeclarationPredicate import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider +import org.jetbrains.kotlin.fir.references.impl.FirSimpleNamedReference import org.jetbrains.kotlin.fir.resolve.toSymbol +import org.jetbrains.kotlin.fir.scopes.impl.toConeType import org.jetbrains.kotlin.fir.symbols.SymbolInternals +import org.jetbrains.kotlin.fir.symbols.impl.ConeClassLikeLookupTagImpl import org.jetbrains.kotlin.fir.types.ConeClassLikeType import org.jetbrains.kotlin.fir.types.ConeKotlinType import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef import org.jetbrains.kotlin.fir.types.FirUserTypeRef import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef import org.jetbrains.kotlin.fir.types.constructClassLikeType +import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl import org.jetbrains.kotlin.fir.types.toSymbol -import org.jetbrains.kotlin.javac.resolve.classId import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName /** * Created by benny at 2023/5/29 14:47. */ class KudosFirSupertypeGenerationExtension( session: FirSession, + private val kudosAnnotationValueMap: HashMap>, ) : FirSupertypeGenerationExtension(session) { private val hasKudos = DeclarationPredicate.create { - annotated(FqName(KUDOS)) + annotated(KUDOS_NAME) } override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(hasKudos) } - context(TypeResolveServiceContainer) override fun computeAdditionalSupertypes( + context(TypeResolveServiceContainer) + override fun computeAdditionalSupertypes( classLikeDeclaration: FirClassLikeDeclaration, resolvedSupertypes: List, ): List { - val kudosValidatorClassId = classId("com.kanyun.kudos.validator", "KudosValidator") + var hasValidator = false + var hasJsonAdapter = false + val annotationValues = classLikeDeclaration.symbol.resolvedAnnotationsWithArguments.getAnnotationByClassId(KUDOS_ANNOTATION_CLASS_ID, session) + ?.getIntArrayArgument() + kudosAnnotationValueMap[classLikeDeclaration.classId.toString()] = annotationValues ?: emptyList() for (superTypeRef in resolvedSupertypes) { val superType = superTypeRef.type val superTypeClassIds = superType.allSuperTypeClassIds() - if (kudosValidatorClassId in superTypeClassIds) return emptyList() + if (KUDOS_VALIDATOR_CLASS_ID in superTypeClassIds) { + hasValidator = true + } + if (KUDOS_JSON_ADAPTER_CLASS_ID in superTypeClassIds) { + hasJsonAdapter = true + } } - return listOf( - buildResolvedTypeRef { - type = kudosValidatorClassId.constructClassLikeType( + val firTypeRefList = mutableListOf() + if (!hasValidator) { + firTypeRefList += buildResolvedTypeRef { + type = KUDOS_VALIDATOR_CLASS_ID.constructClassLikeType( emptyArray(), isNullable = false, ) - }, - ) + } + } + if (Options.isAndroidJsonReaderEnabled(kudosAnnotationValueMap, classLikeDeclaration.classId.toString())) { + if (!hasJsonAdapter) { + val genericType = ConeClassLikeTypeImpl( + ConeClassLikeLookupTagImpl(classLikeDeclaration.classId), + classLikeDeclaration.typeParameters.map { + it.toConeType() + }.toTypedArray(), + false, + ) + firTypeRefList += buildResolvedTypeRef { + type = KUDOS_JSON_ADAPTER_CLASS_ID.constructClassLikeType( + arrayOf(genericType), + isNullable = false, + ) + } + } + } + return firTypeRefList } override fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean { @@ -107,4 +149,27 @@ class KudosFirSupertypeGenerationExtension( else -> null } } + + private fun FirAnnotation.getIntArrayArgument(): List? { + if (this !is FirAnnotationCall) return null + return arguments.map { + when (val annotationValue = it.safeAs()?.calleeReference?.safeAs()?.name?.asString()) { + "KUDOS_ANDROID_JSON_READER" -> { + Options.KUDOS_ANDROID_JSON_READER + } + + "KUDOS_GSON" -> { + Options.KUDOS_GSON + } + + "KUDOS_JACKSON" -> { + Options.KUDOS_JACKSON + } + + else -> { + throw IllegalArgumentException("unknown annotation argument $annotationValue") + } + } + } + } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosPluginKey.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosPluginKey.kt new file mode 100644 index 0000000..1ffa77a --- /dev/null +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k2/KudosPluginKey.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Kanyun, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kanyun.kudos.compiler.k2 + +import org.jetbrains.kotlin.GeneratedDeclarationKey + +object KudosPluginKey : GeneratedDeclarationKey() { + override fun toString(): String { + return "FirKudos" + } +} diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/options/Options.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/options/Options.kt index a343b54..28365fb 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/options/Options.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/options/Options.kt @@ -18,6 +18,11 @@ package com.kanyun.kudos.compiler.options object Options { + const val KUDOS_GSON = 1 + const val KUDOS_JACKSON = 2 + const val KUDOS_ANDROID_JSON_READER = 3 + val validAnnotationList = listOf(KUDOS_GSON, KUDOS_JACKSON, KUDOS_ANDROID_JSON_READER) + @JvmField val gson = Option( "gson", @@ -34,9 +39,37 @@ object Options { "", ) + @JvmField + val androidJsonReader = Option( + "androidJsonReader", + false, + "Whether to enable the support for AndroidJsonReader.", + "", + ) + val all = Options::class.java.declaredFields.filter { it.type == Option::class.java }.map { it.get(null) as Option<*> } + + fun isGsonEnabled(kudosAnnotationValueMap: HashMap>, className: String?): Boolean { + if (className.isNullOrEmpty()) return false + val annotationValue = kudosAnnotationValueMap[className] + return if (annotationValue.isNullOrEmpty()) { + gson() + } else { + annotationValue.contains(KUDOS_GSON) + } + } + + fun isAndroidJsonReaderEnabled(kudosAnnotationValueMap: HashMap>, className: String?): Boolean { + if (className.isNullOrEmpty()) return false + val annotationValue = kudosAnnotationValueMap[className] + return if (annotationValue.isNullOrEmpty()) { + androidJsonReader() + } else { + annotationValue.contains(KUDOS_ANDROID_JSON_READER) + } + } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/utils/IrClass.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/utils/IrClass.kt index bfacc56..0ae008a 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/utils/IrClass.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/utils/IrClass.kt @@ -16,7 +16,7 @@ package com.kanyun.kudos.compiler.utils -import com.kanyun.kudos.compiler.KUDOS +import com.kanyun.kudos.compiler.KudosNames.KUDOS_NAME import org.jetbrains.kotlin.backend.jvm.ir.erasedUpperBound import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.builders.declarations.addFunction @@ -33,7 +33,7 @@ import org.jetbrains.kotlin.name.FqName * Created by Benny Huo on 2023/9/8 */ fun IrClass.hasKudosAnnotation(): Boolean { - return hasAnnotation(FqName(KUDOS)) + return hasAnnotation(KUDOS_NAME) } fun IrClass.isSubclassOfFqName(fqName: String): Boolean = diff --git a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt index 77ee1cd..e5056c1 100644 --- a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt +++ b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt @@ -25,26 +25,56 @@ import org.junit.Test class KudosTests : TestBase() { @Test - fun `classDeclarationCheck`() = testBase() + fun `common_classDeclarationCheck`() = testBase() @Test - fun `constructor`() = testBase() + fun `common_constructor`() = testBase() @Test - fun `defaultValue`() = testBase() + fun `common_defaultValue`() = testBase() + + @Test + fun `common_initBlock`() = testBase() + + @Test + fun `common_notNull`() = testBase() + + @Test + fun `common_propertyTypeCheck`() = testBase() + + @Test + fun `common_validator`() = testBase() + + @Test + fun `gson_annotation`() = testBase() @Test fun `gson_jsonAdapterCheck`() = testBase() @Test - fun `initBlock`() = testBase() + fun `gson_notNull`() = testBase() + + @Test + fun `jsonReader_annotation`() = testBase() + + @Test + fun `jsonReader_deserialize`() = testBase() + + @Test + fun `jsonReader_deserializeArrayType`() = testBase() + + @Test + fun `jsonReader_deserializeFloatType`() = testBase() + + @Test + fun `jsonReader_deserializeMapType`() = testBase() @Test - fun `notNull`() = testBase() + fun `jsonReader_deserializeSetType`() = testBase() @Test - fun `propertyTypeCheck`() = testBase() + fun `jsonReader_notNull`() = testBase() @Test - fun `validator`() = testBase() + fun `jsonReader_simple`() = testBase() } diff --git a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt index 2261572..ab2d483 100644 --- a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt +++ b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt @@ -17,14 +17,13 @@ package com.kanyun.kudos.compiler.base import com.bennyhuo.kotlin.compiletesting.extensions.module.COMPILER_OUTPUT_LEVEL_WARN -import com.bennyhuo.kotlin.compiletesting.extensions.module.IR_OUTPUT_TYPE_RAW +import com.bennyhuo.kotlin.compiletesting.extensions.module.IR_OUTPUT_TYPE_KOTLIN_LIKE_JC import com.bennyhuo.kotlin.compiletesting.extensions.module.KotlinModule import com.bennyhuo.kotlin.compiletesting.extensions.module.checkResult import com.bennyhuo.kotlin.compiletesting.extensions.source.TextBasedModuleInfoLoader import com.kanyun.kudos.compiler.KudosCompilerPluginRegistrar import com.kanyun.kudos.compiler.options.Options import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly import java.io.File /** @@ -33,7 +32,7 @@ import java.io.File @OptIn(ExperimentalCompilerApi::class) open class TestBase { - private val validVariants = arrayOf("gson", "jackson") + private val validVariants = arrayOf("gson", "jackson", "jsonReader") private val useK2 = System.getProperty("KOTLIN_COMPILER") == "K2" private val variant = System.getProperty("VARIANT") ?: validVariants.first() @@ -43,7 +42,9 @@ open class TestBase { } private fun firstExistFile(vararg paths: String): String { - return paths.flatMap { path -> + return paths.map { + it.replace("_", "/") + }.flatMap { path -> // ignore cases of the first letter listOf(path.replaceFirstChar { it.uppercaseChar() }, path.replaceFirstChar { it.lowercaseChar() }) }.firstOrNull { @@ -67,8 +68,9 @@ open class TestBase { import com.google.gson.annotations.JsonAdapter import com.kanyun.kudos.gson.kudosGson import com.kanyun.kudos.gson.adapter.KudosReflectiveTypeAdapterFactory + import java.lang.reflect.Type - inline fun deserialize(string: String): T? { + inline fun deserialize(string: String, type: Type = T::class.java): T? { val gson = kudosGson() return try { val t: T = gson.fromJson(string, object: TypeToken() {}.type) @@ -88,8 +90,9 @@ open class TestBase { import com.kanyun.kudos.jackson.kudosObjectMapper import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.DeserializationFeature + import java.lang.reflect.Type - inline fun deserialize(string: String): T? { + inline fun deserialize(string: String, type: Type = T::class.java): T? { val mapper = kudosObjectMapper() mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.disable(DeserializationFeature.WRAP_EXCEPTIONS); @@ -105,6 +108,26 @@ open class TestBase { """.trimIndent() } + private fun jsonReaderDeserialize(): String { + Options.androidJsonReader.set(true) + return """ + // FILE: JsonReader.kt + import com.kanyun.kudos.json.reader.KudosAndroidJsonReader + import java.lang.reflect.Type + + inline fun deserialize(string: String, type: Type = T::class.java): T? { + return try { + val t: T = KudosAndroidJsonReader.fromJson(string, type) + println(t) + t + } catch (e: Exception) { + println(e) + null + } + } + """.trimIndent() + } + private fun doTest(fileName: String, deserializer: () -> String) { val filePath = firstExistFile("testData/$fileName", "testData/$fileName.kt", "testData/$fileName.txt") val deserializerSource = deserializer() @@ -127,7 +150,7 @@ open class TestBase { executeEntries = true, checkCompilerOutput = true, compilerOutputLevel = COMPILER_OUTPUT_LEVEL_WARN, - irOutputType = IR_OUTPUT_TYPE_RAW, + irOutputType = IR_OUTPUT_TYPE_KOTLIN_LIKE_JC, ) } @@ -138,13 +161,16 @@ open class TestBase { return } - when (variant.toLowerCaseAsciiOnly()) { + when (variant) { "gson" -> { doTest(fileName, ::gsonDeserialize) } "jackson" -> { doTest(fileName, ::jacksonDeserialize) } + "jsonReader" -> { + doTest(fileName, ::jsonReaderDeserialize) + } else -> { throw UnsupportedOperationException("Unknown variant '$variant'. Supported values: 'gson', 'jackson'.") } diff --git a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/generator/TestsGenerator.kt b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/generator/TestsGenerator.kt index e5b8b04..f733abc 100644 --- a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/generator/TestsGenerator.kt +++ b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/generator/TestsGenerator.kt @@ -61,13 +61,28 @@ fun main() { * Created by Benny Huo */ class KudosTests : TestBase() { - - ${caseDir.listFiles().orEmpty() - .map { it.nameWithoutExtension } - .sortedBy { it } - .joinToString("\n") { - "@Test\nfun `${it.replaceFirstChar { it.lowercaseChar() }}`() = testBase()\n" - } + + ${ + caseDir.listFiles().orEmpty() + .flatMap { + if (it.isDirectory) { + it.listFiles().orEmpty().toList() + } else { + listOf(it) + } + } + .map { + val parentDir = it.parentFile.name + if (parentDir == "testData") { + it.nameWithoutExtension + } else { + "${parentDir}_${it.nameWithoutExtension}" + } + } + .sortedBy { it } + .joinToString("\n") { + "@Test\nfun `${it.replaceFirstChar { it.lowercaseChar() }}`() = testBase()\n" + } } } """.trimIndent() diff --git a/kudos-compiler/testData/classDeclarationCheck.kt b/kudos-compiler/testData/common/classDeclarationCheck.kt similarity index 100% rename from kudos-compiler/testData/classDeclarationCheck.kt rename to kudos-compiler/testData/common/classDeclarationCheck.kt diff --git a/kudos-compiler/testData/constructor.kt b/kudos-compiler/testData/common/constructor.kt similarity index 100% rename from kudos-compiler/testData/constructor.kt rename to kudos-compiler/testData/common/constructor.kt diff --git a/kudos-compiler/testData/defaultValue.kt b/kudos-compiler/testData/common/defaultValue.kt similarity index 100% rename from kudos-compiler/testData/defaultValue.kt rename to kudos-compiler/testData/common/defaultValue.kt diff --git a/kudos-compiler/testData/initBlock.kt b/kudos-compiler/testData/common/initBlock.kt similarity index 100% rename from kudos-compiler/testData/initBlock.kt rename to kudos-compiler/testData/common/initBlock.kt diff --git a/kudos-compiler/testData/notNull.kt b/kudos-compiler/testData/common/notNull.kt similarity index 69% rename from kudos-compiler/testData/notNull.kt rename to kudos-compiler/testData/common/notNull.kt index 0961a68..3d92aca 100644 --- a/kudos-compiler/testData/notNull.kt +++ b/kudos-compiler/testData/common/notNull.kt @@ -1,3 +1,5 @@ +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + // SOURCE {{deserialize}} // FILE: Main.kt [MainKt#main] @@ -5,6 +7,7 @@ import com.kanyun.kudos.annotations.Kudos import com.kanyun.kudos.collections.KudosCollection import com.kanyun.kudos.collections.KudosList import com.kanyun.kudos.collections.KudosSet +import com.kanyun.kudos.json.reader.adapter.ParameterizedTypeImpl @Kudos data class User(val id: Long, val name: String, val age: Int = 10) { @@ -28,18 +31,24 @@ class Arrays( } } +@Kudos +class Project(val id: Int,val projectDesc: Desc) + +@Kudos +class Desc(val des: String) + fun main() { deserialize("""{}""") deserialize("""{"id": 10}""") - deserialize("""{"name": "Bod"}""") - deserialize>("""[{"id": 10}, {"id": 11}]""") - deserialize>("""[{"id": 10, "name": "Bob"}]""") - deserialize>("""[null]""") - deserialize>("""[null]""") - deserialize>("""[null]""") + deserialize("""{"name": "Bob"}""") + deserialize>("""[{"id": 10}, {"id": 11}]""", ParameterizedTypeImpl(List::class.java, arrayOf(User::class.java))) + deserialize>("""[{"id": 10, "name": "Bob"}]""", ParameterizedTypeImpl(KudosCollection::class.java, arrayOf(User::class.java))) + deserialize>("""[null]""", ParameterizedTypeImpl(KudosCollection::class.java, arrayOf(User::class.java))) + deserialize>("""[null]""", ParameterizedTypeImpl(KudosList::class.java, arrayOf(User::class.java))) + deserialize>("""[null]""", ParameterizedTypeImpl(KudosSet::class.java, arrayOf(User::class.java))) // Maybe supported with Java 8 annotated type. But ... not the moment. - deserialize>("""[null]""") + deserialize>("""[null]""", ParameterizedTypeImpl(List::class.java, arrayOf(User::class.java))) deserialize("""{"name": "Bob"}""") deserialize("""{"id": 10, "name": "Bob"}""") @@ -66,4 +75,4 @@ User(id=10, name=Bob, age=10) java.lang.NullPointerException: Element must not be null in List 'list'. Collections(list=[kudos], list2=[null], list3=null) java.lang.NullPointerException: Element must not be null in array 'array'. -Arrays(array=[kudos], array2=[null], array3=null) +Arrays(array=[kudos], array2=[null], array3=null) \ No newline at end of file diff --git a/kudos-compiler/testData/propertyTypeCheck.kt b/kudos-compiler/testData/common/propertyTypeCheck.kt similarity index 100% rename from kudos-compiler/testData/propertyTypeCheck.kt rename to kudos-compiler/testData/common/propertyTypeCheck.kt diff --git a/kudos-compiler/testData/validator.kt b/kudos-compiler/testData/common/validator.kt similarity index 94% rename from kudos-compiler/testData/validator.kt rename to kudos-compiler/testData/common/validator.kt index 3ae6779..558b364 100644 --- a/kudos-compiler/testData/validator.kt +++ b/kudos-compiler/testData/common/validator.kt @@ -62,7 +62,7 @@ import com.kanyun.kudos.test.User fun main() { (User() as KudosValidator).validate(emptyMap()) - (Developer("kanyun") as KudosValidator).validate(emptyMap()) + (Developer("kanyun") as KudosValidator).validate(hashMapOf("company" to true)) } // EXPECT diff --git a/kudos-compiler/testData/gson/annotation.kt b/kudos-compiler/testData/gson/annotation.kt new file mode 100644 index 0000000..d318861 --- /dev/null +++ b/kudos-compiler/testData/gson/annotation.kt @@ -0,0 +1,44 @@ +// SOURCE +// FILE: Main.kt +package com.kanyun.kudos.test + +import com.kanyun.kudos.annotations.Kudos +import com.kanyun.kudos.gson.KUDOS_GSON + +@Kudos(KUDOS_GSON) +class Desc(val descDetail: String) + +@Kudos(KUDOS_GSON) +class Project(val projectName: String, val projectId: Int, val tags: List,val desc: Desc) + +// EXPECT +// FILE: compiles.log +OK +// FILE: Main.kt.ir +package com.kanyun.kudos.test +@Kudos(value = 1) +@JsonAdapter(value = KudosReflectiveTypeAdapterFactory::class) +class Desc(val descDetail: String) : KudosValidator { + constructor{ + ctor() + init() + } + override fun validate(status: Map) { + validateField("descDetail", status) + } +} +@Kudos(value = 1) +@JsonAdapter(value = KudosReflectiveTypeAdapterFactory::class) +class Project(val projectName: String, val projectId: Int, val tags: List, val desc: Desc) : KudosValidator { + constructor{ + ctor() + init() + } + override fun validate(status: Map) { + validateField("projectName", status) + validateField("projectId", status) + validateField("tags", status) + validateField("desc", status) + validateCollection("tags", .tags, "List") + } +} \ No newline at end of file diff --git a/kudos-compiler/testData/gson_jsonAdapterCheck.kt b/kudos-compiler/testData/gson/jsonAdapterCheck.kt similarity index 100% rename from kudos-compiler/testData/gson_jsonAdapterCheck.kt rename to kudos-compiler/testData/gson/jsonAdapterCheck.kt diff --git a/kudos-compiler/testData/gson/notNull.kt b/kudos-compiler/testData/gson/notNull.kt new file mode 100644 index 0000000..a66fc37 --- /dev/null +++ b/kudos-compiler/testData/gson/notNull.kt @@ -0,0 +1,46 @@ +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos +import com.kanyun.kudos.collections.KudosCollection +import com.kanyun.kudos.collections.KudosList +import com.kanyun.kudos.collections.KudosSet +import com.kanyun.kudos.json.reader.adapter.ParameterizedTypeImpl + +@Kudos +data class User(val id: Long, val name: String, val age: Int = 10) { + val city: String = "beijing" +} + +@Kudos +data class Collections( + val list: List, + val list2: List, + val list3: List?, +) +@Kudos +class Arrays( + val array: Array, + val array2: Array, + val array3: Array?, +) { + override fun toString(): String { + return "Arrays(array=${array.contentToString()}, array2=${array2.contentToString()}, array3=${array3?.contentToString()})" + } +} + +@Kudos +class Project(val id: Int,val projectDesc: Desc) + +@Kudos +class Desc(val des: String) + +fun main() { + deserialize("""{"id": 10, "projectDesc": null}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +java.lang.NullPointerException: Missing non-null field 'projectDesc'. \ No newline at end of file diff --git a/kudos-compiler/testData/jsonReader/annotation.kt b/kudos-compiler/testData/jsonReader/annotation.kt new file mode 100644 index 0000000..a57a9f0 --- /dev/null +++ b/kudos-compiler/testData/jsonReader/annotation.kt @@ -0,0 +1,100 @@ +// SOURCE +// FILE: Main.kt +package com.kanyun.kudos.test + +import com.kanyun.kudos.annotations.Kudos +import com.kanyun.kudos.json.reader.KUDOS_ANDROID_JSON_READER + +@Kudos(KUDOS_ANDROID_JSON_READER) +class Desc(val descDetail: String) + +@Kudos(KUDOS_ANDROID_JSON_READER) +class Project(val projectName: String, val projectId: Int, val tags: List,val desc: Desc) + +// EXPECT +// FILE: compiles.log +OK +// FILE: Main.kt.ir +package com.kanyun.kudos.test +@Kudos(value = 3) +class Desc(val descDetail: String) : KudosValidator, KudosJsonAdapter { + override fun fromJson(jsonReader: JsonReader): Desc { + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val tmp0 = jsonReader.nextName() + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + continue + } + when { + tmp0 == "descDetail" -> { + .descDetail = jsonReader.nextString() + .kudosFieldStatusMap.put("descDetail", .descDetail != null) + } + else -> { + jsonReader.skipValue() + } + } + } + jsonReader.endObject() + validate(.kudosFieldStatusMap) + return + } + constructor{ + ctor() + init() + } + override fun validate(status: Map) { + validateField("descDetail", status) + } + private var kudosFieldStatusMap: Map = hashMapOf() +} +@Kudos(value = 3) +class Project(val projectName: String, val projectId: Int, val tags: List, val desc: Desc) : KudosValidator, KudosJsonAdapter { + override fun fromJson(jsonReader: JsonReader): Project { + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val tmp0 = jsonReader.nextName() + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + continue + } + when { + tmp0 == "projectName" -> { + .projectName = jsonReader.nextString() + .kudosFieldStatusMap.put("projectName", .projectName != null) + } + tmp0 == "projectId" -> { + .projectId = jsonReader.nextInt() + .kudosFieldStatusMap.put("projectId", .projectId != null) + } + tmp0 == "tags" -> { + .tags = parseKudosObject(jsonReader, ParameterizedTypeImpl(List::class.javaObjectType, arrayOf(String::class.javaObjectType))) + .kudosFieldStatusMap.put("tags", .tags != null) + } + tmp0 == "desc" -> { + .desc = parseKudosObject(jsonReader, Desc::class.javaObjectType) + .kudosFieldStatusMap.put("desc", .desc != null) + } + else -> { + jsonReader.skipValue() + } + } + } + jsonReader.endObject() + validate(.kudosFieldStatusMap) + return + } + constructor{ + ctor() + init() + } + override fun validate(status: Map) { + validateField("projectName", status) + validateField("projectId", status) + validateField("tags", status) + validateField("desc", status) + validateCollection("tags", .tags, "List") + } + private var kudosFieldStatusMap: Map = hashMapOf() +} \ No newline at end of file diff --git a/kudos-compiler/testData/jsonReader/deserialize.kt b/kudos-compiler/testData/jsonReader/deserialize.kt new file mode 100644 index 0000000..b89d79a --- /dev/null +++ b/kudos-compiler/testData/jsonReader/deserialize.kt @@ -0,0 +1,29 @@ +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos +@Kudos +class Desc(val descDetail: String, val descId: Int) + +@Kudos +class UserLazy(val id: Long, val name: String, val desc: Desc, val tags: List>) { + val firstName by lazy { + name.split(" ").first() + } + + val lastName by lazy { + name.split(" ").last() + } + + override fun toString(): String { + return "UserLazy(id=$id, name=$name, firstName=$firstName, lastName=$lastName, desc=${desc.descDetail}, tags=$tags" + } +} + +fun main() { + deserialize("""{"id": 10, "name": "John Claud", "desc": {"descDetail": "desc detail", "descId": 123}, "tags": [["tag1", "tag2"],["abc","def"]] }""") +} + +// EXPECT +// FILE: MainKt.main.stdout +UserLazy(id=10, name=John Claud, firstName=John, lastName=Claud, desc=desc detail, tags=[[tag1, tag2], [abc, def]] diff --git a/kudos-compiler/testData/jsonReader/deserializeArrayType.kt b/kudos-compiler/testData/jsonReader/deserializeArrayType.kt new file mode 100644 index 0000000..592d166 --- /dev/null +++ b/kudos-compiler/testData/jsonReader/deserializeArrayType.kt @@ -0,0 +1,20 @@ +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos + +@Kudos +class User(val ids: Array) { + + override fun toString(): String { + return "User(ids[1]=${ids[1]})" + } +} + +fun main() { + deserialize("""{"ids": [123, 456]}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +User(ids[1]=456) diff --git a/kudos-compiler/testData/jsonReader/deserializeFloatType.kt b/kudos-compiler/testData/jsonReader/deserializeFloatType.kt new file mode 100644 index 0000000..37b13b8 --- /dev/null +++ b/kudos-compiler/testData/jsonReader/deserializeFloatType.kt @@ -0,0 +1,20 @@ +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos + +@Kudos +class User(val doubleId: Double, val floatId: Float) { + + override fun toString(): String { + return "User(doubleId=$doubleId, floatId=$floatId)" + } +} + +fun main() { + deserialize("""{"doubleId": 10.1234, "floatId": "10.1234"}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +User(doubleId=10.1234, floatId=10.1234) diff --git a/kudos-compiler/testData/jsonReader/deserializeMapType.kt b/kudos-compiler/testData/jsonReader/deserializeMapType.kt new file mode 100644 index 0000000..c276bd0 --- /dev/null +++ b/kudos-compiler/testData/jsonReader/deserializeMapType.kt @@ -0,0 +1,38 @@ +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos + +@Kudos +class User(val id: Int, val tag: String){ + override fun toString(): String { + return "User(id=${id}, tag=${tag})" + } +} + +@Kudos +class UserMap(val itemMap: Map) { + + override fun toString(): String { + return "UserMap(user2=${itemMap["user2"]})" + } +} + +fun main() { + deserialize("""{ + "itemMap": { + "user1": { + "id": 123, + "tag": "tag1" + }, + "user2": { + "id": 456, + "tag": "tag2" + } + } +}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +UserMap(user2=User(id=456, tag=tag2)) diff --git a/kudos-compiler/testData/jsonReader/deserializeSetType.kt b/kudos-compiler/testData/jsonReader/deserializeSetType.kt new file mode 100644 index 0000000..58582ec --- /dev/null +++ b/kudos-compiler/testData/jsonReader/deserializeSetType.kt @@ -0,0 +1,20 @@ +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos + +@Kudos +class User(val ids: Set) { + + override fun toString(): String { + return "User(ids=${ids})" + } +} + +fun main() { + deserialize("""{"ids": [123, 456, 123]}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +User(ids=[123, 456]) diff --git a/kudos-compiler/testData/jsonReader/notNull.kt b/kudos-compiler/testData/jsonReader/notNull.kt new file mode 100644 index 0000000..6f276d7 --- /dev/null +++ b/kudos-compiler/testData/jsonReader/notNull.kt @@ -0,0 +1,24 @@ +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos +import com.kanyun.kudos.collections.KudosCollection +import com.kanyun.kudos.collections.KudosList +import com.kanyun.kudos.collections.KudosSet +import com.kanyun.kudos.json.reader.adapter.ParameterizedTypeImpl + +@Kudos +class Project(val id: Int,val projectDesc: Desc) + +@Kudos +class Desc(val des: String) + +fun main() { + deserialize("""{"id": 10, "projectDesc": null}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +java.lang.NullPointerException: Missing non-null field 'projectDesc'. \ No newline at end of file diff --git a/kudos-compiler/testData/jsonReader/simple.kt b/kudos-compiler/testData/jsonReader/simple.kt new file mode 100644 index 0000000..77a24be --- /dev/null +++ b/kudos-compiler/testData/jsonReader/simple.kt @@ -0,0 +1,99 @@ +// SOURCE +// FILE: Main.kt +package com.kanyun.kudos.test + +import com.kanyun.kudos.annotations.Kudos + +@Kudos +class Desc(val descDetail: String) + +@Kudos +class Project(val projectName: String, val projectId: Int, val tags: List,val desc: Desc) + +// EXPECT +// FILE: compiles.log +OK +// FILE: Main.kt.ir +package com.kanyun.kudos.test +@Kudos(value = ) +class Desc(val descDetail: String) : KudosValidator, KudosJsonAdapter { + override fun fromJson(jsonReader: JsonReader): Desc { + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val tmp0 = jsonReader.nextName() + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + continue + } + when { + tmp0 == "descDetail" -> { + .descDetail = jsonReader.nextString() + .kudosFieldStatusMap.put("descDetail", .descDetail != null) + } + else -> { + jsonReader.skipValue() + } + } + } + jsonReader.endObject() + validate(.kudosFieldStatusMap) + return + } + constructor{ + ctor() + init() + } + override fun validate(status: Map) { + validateField("descDetail", status) + } + private var kudosFieldStatusMap: Map = hashMapOf() +} +@Kudos(value = ) +class Project(val projectName: String, val projectId: Int, val tags: List, val desc: Desc) : KudosValidator, KudosJsonAdapter { + override fun fromJson(jsonReader: JsonReader): Project { + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val tmp0 = jsonReader.nextName() + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + continue + } + when { + tmp0 == "projectName" -> { + .projectName = jsonReader.nextString() + .kudosFieldStatusMap.put("projectName", .projectName != null) + } + tmp0 == "projectId" -> { + .projectId = jsonReader.nextInt() + .kudosFieldStatusMap.put("projectId", .projectId != null) + } + tmp0 == "tags" -> { + .tags = parseKudosObject(jsonReader, ParameterizedTypeImpl(List::class.javaObjectType, arrayOf(String::class.javaObjectType))) + .kudosFieldStatusMap.put("tags", .tags != null) + } + tmp0 == "desc" -> { + .desc = parseKudosObject(jsonReader, Desc::class.javaObjectType) + .kudosFieldStatusMap.put("desc", .desc != null) + } + else -> { + jsonReader.skipValue() + } + } + } + jsonReader.endObject() + validate(.kudosFieldStatusMap) + return + } + constructor{ + ctor() + init() + } + override fun validate(status: Map) { + validateField("projectName", status) + validateField("projectId", status) + validateField("tags", status) + validateField("desc", status) + validateCollection("tags", .tags, "List") + } + private var kudosFieldStatusMap: Map = hashMapOf() +} \ No newline at end of file diff --git a/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosExtension.kt b/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosExtension.kt index 88fa835..9830bc2 100644 --- a/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosExtension.kt +++ b/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosExtension.kt @@ -22,4 +22,5 @@ package com.kanyun.kudos.gradle open class KudosExtension { var gson: Boolean = false var jackson: Boolean = false + var androidJsonReader: Boolean = false } diff --git a/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosGradlePlugin.kt b/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosGradlePlugin.kt index 9e63e8f..bfefec4 100644 --- a/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosGradlePlugin.kt +++ b/kudos-gradle-plugin/src/main/java/com/kanyun/kudos/gradle/KudosGradlePlugin.kt @@ -46,7 +46,9 @@ open class KudosGradlePlugin : KotlinCompilerPluginSupportPlugin { if (kudosExtension.jackson) { config += "${BuildConfig.KOTLIN_PLUGIN_GROUP}:kudos-jackson:${BuildConfig.KOTLIN_PLUGIN_VERSION}" } - + if (kudosExtension.androidJsonReader) { + config += "${BuildConfig.KOTLIN_PLUGIN_GROUP}:kudos-android-json-reader:${BuildConfig.KOTLIN_PLUGIN_VERSION}" + } config += "${BuildConfig.KOTLIN_PLUGIN_GROUP}:kudos-annotations:${BuildConfig.KOTLIN_PLUGIN_VERSION}" config += "${BuildConfig.KOTLIN_PLUGIN_GROUP}:kudos-runtime:${BuildConfig.KOTLIN_PLUGIN_VERSION}" } @@ -75,6 +77,9 @@ open class KudosGradlePlugin : KotlinCompilerPluginSupportPlugin { if (kudosExtension.jackson) { options += SubpluginOption("jackson", "true") } + if (kudosExtension.androidJsonReader) { + options += SubpluginOption("androidJsonReader", "true") + } return project.provider { options } } } diff --git a/kudos-gson/src/main/java/com/kanyun/kudos/gson/KudosGson.kt b/kudos-gson/src/main/java/com/kanyun/kudos/gson/KudosGson.kt index c99a31d..dee29fa 100644 --- a/kudos-gson/src/main/java/com/kanyun/kudos/gson/KudosGson.kt +++ b/kudos-gson/src/main/java/com/kanyun/kudos/gson/KudosGson.kt @@ -29,3 +29,5 @@ fun kudosGsonBuilder(): GsonBuilder { fun kudosGson(): Gson { return kudosGsonBuilder().create() } + +const val KUDOS_GSON: Int = 1 diff --git a/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java b/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java index f594f0b..9dd9a2c 100644 --- a/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java +++ b/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java @@ -40,8 +40,10 @@ public void write(JsonWriter writer, Object value) throws IOException, IllegalAc public void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { Object fieldValue = typeAdapter.read(reader); - if (fieldValue != null || !field.getType().isPrimitive()) { + if (fieldValue != null) { isInitialized = true; + } + if (fieldValue != null || !field.getType().isPrimitive()) { field.set(value, fieldValue); } } diff --git a/kudos-jackson/src/main/java/com/kanyun/kudos/jackson/KudosObjectMapper.kt b/kudos-jackson/src/main/java/com/kanyun/kudos/jackson/KudosObjectMapper.kt index 611697a..752b5d4 100644 --- a/kudos-jackson/src/main/java/com/kanyun/kudos/jackson/KudosObjectMapper.kt +++ b/kudos-jackson/src/main/java/com/kanyun/kudos/jackson/KudosObjectMapper.kt @@ -42,3 +42,5 @@ fun kudosObjectMapper( } return ObjectMapper(jf, sp, deserializationContext) } + +const val KUDOS_JACKSON: Int = 2 diff --git a/kudos-runtime/build.gradle.kts b/kudos-runtime/build.gradle.kts index 2bbcd34..b1e8042 100644 --- a/kudos-runtime/build.gradle.kts +++ b/kudos-runtime/build.gradle.kts @@ -17,6 +17,6 @@ plugins { java kotlin("jvm") } - dependencies { } + diff --git a/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt b/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt index 1c1b4af..1846a62 100644 --- a/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt +++ b/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt @@ -24,7 +24,7 @@ interface KudosValidator { } fun validateField(name: String, fieldStatus: Map) { - if (fieldStatus[name] == false) { + if (fieldStatus[name] != true) { throw NullPointerException("Missing non-null field '$name'.") } } diff --git a/kudos-sample/kudos-gradle-sample/build.gradle.kts b/kudos-sample/kudos-gradle-sample/build.gradle.kts index 2aff3cd..8f5cd9d 100644 --- a/kudos-sample/kudos-gradle-sample/build.gradle.kts +++ b/kudos-sample/kudos-gradle-sample/build.gradle.kts @@ -25,6 +25,8 @@ repositories { maven("https://s01.oss.sonatype.org/content/repositories/snapshots") } +val KUDOS_VERSION: String by extra + kudos { gson = true jackson = true @@ -33,6 +35,7 @@ kudos { dependencies { implementation("com.google.code.gson:gson:2.10") implementation("com.fasterxml.jackson.core:jackson-databind:2.15.0") + implementation("com.kanyun.kudos:android-json-reader:$KUDOS_VERSION") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("junit:junit:4.13.1") } diff --git a/kudos-sample/kudos-maven-sample/pom.xml b/kudos-sample/kudos-maven-sample/pom.xml index 99583af..0cacadf 100644 --- a/kudos-sample/kudos-maven-sample/pom.xml +++ b/kudos-sample/kudos-maven-sample/pom.xml @@ -34,6 +34,11 @@ kudos-jackson ${kudos.version} + + com.kanyun.kudos + kudos-android-json-reader + ${kudos.version} + com.google.code.gson gson @@ -44,6 +49,11 @@ jackson-databind 2.15.0 + + com.kanyun.kudos + android-json-reader + ${kudos.version} + org.jetbrains.kotlin kotlin-test-junit diff --git a/settings.gradle.kts b/settings.gradle.kts index 1fb009d..1cc7ae7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,5 @@ include(":kudos-annotations") include(":kudos-runtime") include(":kudos-gson") include(":kudos-jackson") +include(":kudos-android-json-reader") +include(":android-json-reader")