Initial commit

This commit is contained in:
2025-10-07 07:39:24 +08:00
commit afa0ee5c2e
74 changed files with 3140 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.maven.publish)
}
group = property.project.groupName
version = property.project.version
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
jvmToolchain(17)
compilerOptions {
freeCompilerArgs = listOf(
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions"
)
}
}
dependencies {
implementation(libs.moshi.kotlin)
implementation(libs.kavaref.core)
implementation(libs.kavaref.extension)
}

View File

@@ -0,0 +1,121 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
package com.highcapable.moshi.companion.api
import com.highcapable.kavaref.KavaRef.Companion.resolve
import com.highcapable.kavaref.extension.classOf
import com.highcapable.kavaref.extension.toClassOrNull
import com.highcapable.kavaref.resolver.ConstructorResolver
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
* [MoshiCompanion] registered [JsonAdapter.Factory] implementation that loads
* generated [JsonAdapter]s from the given [MoshiCompanion.AdapterRegistry].
*/
internal class AdapterRegistryFactory(private val adapterRegistry: MoshiCompanion.AdapterRegistry) : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = type.toClassOrNull() ?: return null
// Check if there is annotation for `@JsonClass` and `generateAdapter = true`.
val jsonClass = rawType.getAnnotation(classOf<JsonClass>())
if (jsonClass == null || !jsonClass.generateAdapter) return null
var possiblyFoundAdapter: Class<out JsonAdapter<*>>? = null
return try {
val adapterClass = adapterRegistry.adapters.firstNotNullOfOrNull { (regType, regAdapter) ->
if (Types.equals(rawType, regType))
regAdapter
else null
} ?: run {
// Fallback to reflective lookup.
val rawAdapterClassName = Types.generatedJsonAdapterName(rawType.name)
rawAdapterClassName.toClassOrNull<JsonAdapter<*>>() ?: throw ClassNotFoundException()
}
possiblyFoundAdapter = adapterClass
val constructor: ConstructorResolver<out JsonAdapter<*>>
val args: Array<Any>
if (type is ParameterizedType) {
val typeArgs = type.actualTypeArguments
// Common case first.
val twoParams = adapterClass.resolve()
.optional(silent = true)
.constructor {
parameters(Moshi::class, Array<Type>::class)
}.firstOrNull()
val oneParam = adapterClass.resolve()
.optional(silent = true)
.constructor { parameters(Moshi::class) }
.firstOrNull()
constructor = twoParams ?: oneParam ?: throw NoSuchMethodException()
args = if (twoParams != null)
arrayOf(moshi, typeArgs)
else arrayOf(moshi)
} else {
val oneParam = adapterClass.resolve()
.optional(silent = true)
.constructor { parameters(Moshi::class) }
.firstOrNull()
val noParams = adapterClass.resolve()
.optional(silent = true)
.constructor { emptyParameters() }
.firstOrNull()
constructor = oneParam ?: noParams ?: throw NoSuchMethodException()
args = if (oneParam != null)
arrayOf(moshi)
else emptyArray()
}
constructor.createQuietly(*args)?.nullSafe()
} catch (_: ClassNotFoundException) {
// If it is not found at all, return null and let other factory handle it.
null
} catch (e: NoSuchMethodException) {
if (possiblyFoundAdapter != null && type !is ParameterizedType && possiblyFoundAdapter.typeParameters.isNotEmpty())
throw RuntimeException(
"Failed to find the generated JsonAdapter constructor for '$type'. " +
"Suspiciously, the type was not parameterized but the target class " +
"'${possiblyFoundAdapter.canonicalName}' is generic. " +
"Consider using Types#newParameterizedType() to define these missing type variables.",
e
)
else throw RuntimeException(
"Failed to find the generated JsonAdapter constructor for $type. " +
"Target adapter class is '${possiblyFoundAdapter?.canonicalName}'.",
e
)
} catch (e: Exception) {
throw RuntimeException("Failed to create JsonAdapter for $type.", e)
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
package com.highcapable.moshi.companion.api
import com.highcapable.moshi.companion.api.MoshiCompanion.AdapterRegistry
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Moshi.Builder
import java.lang.reflect.Type
/**
* Moshi Companion core API.
*/
object MoshiCompanion {
/**
* Add an [AdapterRegistry] on this [Moshi.Builder] instance to register all [JsonAdapter]s
* declared in the [AdapterRegistry.adapters] map when [Moshi.Builder.build] is called.
* @see Moshi.Builder.addRegistry
* @param builder the instance.
* @param adapterRegistry the [AdapterRegistry] instance to set.
* @return [Moshi.Builder]
*/
@JvmStatic
fun addRegistry(builder: Builder, adapterRegistry: AdapterRegistry) = apply {
builder.add(AdapterRegistryFactory(adapterRegistry))
}
/**
* A registry of [JsonAdapter]s to be registered with a [Moshi] instance.
*
* To use, create a subclass and implement [adapters]. Then add an instance of your subclass on
* [Moshi.Builder.addRegistry].
*
* Example:
* ```
* class MyAdapterRegistry : MoshiCompanion.AdapterRegistry {
*
* override val adapters = mapOf(
* MyType::class.java to MyTypeJsonAdapter::class.java,
* MyType.SubType::class.java to MyType_SubTypeJsonAdapter::class.java
* )
* }
*
* val myRegistry = MyAdapterRegistry()
*
* val builder = Moshi.Builder().addRegistry(myRegistry)
* val moshi = builder.build()
* ```
*/
interface AdapterRegistry {
/**
* A map of [Type] to [JsonAdapter] to be registered.
*/
val adapters: Map<Type, Class<out JsonAdapter<*>>>
}
}
/**
* Add an [AdapterRegistry] on this [Moshi.Builder] instance to register all [JsonAdapter]s
* declared in the [AdapterRegistry.adapters] map when [Moshi.Builder.build] is called.
* @see MoshiCompanion.addRegistry
* @receiver [Moshi.Builder] instance.
* @param adapterRegistry the [AdapterRegistry] instance to set.
* @return [Moshi.Builder]
*/
@JvmSynthetic
fun Builder.addRegistry(adapterRegistry: AdapterRegistry) = apply {
add(AdapterRegistryFactory(adapterRegistry))
}
/**
* Returns a JSON adapter for reified type parameter [T], creating it if necessary.
*
* Usage:
*
* ```kotlin
* val moshi = Moshi.Builder().build()
* val adapter = moshi.typeAdapter<List<String>>()
* ```
* @receiver [Moshi] instance.
* @see typeRef
* @return [JsonAdapter]<[T]>
*/
inline fun <reified T : Any> Moshi.typeAdapter(): JsonAdapter<T> = adapter(typeRef<T>().type)

View File

@@ -0,0 +1,96 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/6.
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.highcapable.moshi.companion.api
import com.highcapable.kavaref.extension.classOf
import com.highcapable.kavaref.extension.toClass
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
* Type reference class for getting generic parameter [T] type.
*
* The purpose of this class is to retain erased generics at runtime.
* @see typeRef
*/
abstract class TypeRef<T : Any> {
/**
* Get the generic parameter [T] type.
* @return [Type]
*/
val type by lazy {
when (val superclass = javaClass.genericSuperclass) {
is ParameterizedType ->
if (superclass.rawType == classOf<TypeRef<*>>())
superclass.actualTypeArguments.firstOrNull() ?: error("Type argument cannot be null.")
else error("Must only create direct subclasses of TypeRef.")
classOf<TypeRef<*>>() -> error("TypeRef must be created with a type argument: object : TypeRef<...>() {}.")
else -> error("Must only create direct subclasses of TypeRef.")
}
}
/**
* Get the raw class type of the generic parameter [T].
* @return [Class]
*/
val rawType by lazy {
when (val currentType = type) {
is Class<*> -> currentType
is ParameterizedType -> currentType.toClass()
else -> classOf<Any>()
}
}
/**
* Checks if the specified [other] type can be assigned to this type.
* @param other the type to check.
* @return `true` if the specified type can be assigned to this type, `false` otherwise.
*/
fun isAssignableFrom(other: Type) = when {
type is Class<*> && other is Class<*> -> rawType.isAssignableFrom(other)
else -> type == other
}
override fun toString() = type.toString()
override fun equals(other: Any?) = other is TypeRef<*> && type == other.type
override fun hashCode() = type.hashCode()
}
/**
* Create a [TypeRef] instance with the reified type parameter [T].
*
* Usage:
*
* ```kotlin
* val typeRef = typeRef<List<String>>()
* // This will be of type `List<String>`.
* val type = typeRef.type
* // This will be of type `List`.
* val rawType = typeRef.rawType
* ```
* @see TypeRef
* @return [TypeRef]<[T]>
*/
inline fun <reified T : Any> typeRef() = object : TypeRef<T>() {}