Created
February 7, 2018 17:25
-
-
Save luciofm/bc2ee4c5cc17a9a69c3b88cfb64ff4c8 to your computer and use it in GitHub Desktop.
FallbackSealedClass inpired by FallbackEnum (https://github.com/serj-lotutovici/moshi-lazy-adapters/blob/master/src/main/java/com/serjltt/moshi/adapters/FallbackEnum.java)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2018 Lucio Maciel, Rocket.Chat | |
* Copyright 2016 Serj Lotutovici | |
* | |
* 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. | |
*/ | |
import com.squareup.moshi.Moshi | |
import java.lang.annotation.Documented | |
/** | |
* Indicates that the annotated sealed class has a fallback value. The fallback must be set via | |
* [.name] and must have a constructor String field [.fieldName]. If no class with the provided name is declared in the | |
* annotated sealed class type an [assertion error][AssertionError] will be thrown. | |
* | |
* | |
* To leverage from [FallbackSealedClass] [FallbackSealedClassJsonAdapter.ADAPTER_FACTORY] must be added to | |
* your [moshi instance][Moshi]: | |
* | |
* | |
* <pre>` | |
* Moshi moshi = new Moshi.Builder() | |
* .add(FallbackEnum.ADAPTER_FACTORY) | |
* .build(); | |
* `</pre> | |
* | |
* Declaration example: | |
* <pre>` | |
* @FallbackSealedClass(name = "Custom", fieldName = "rawType") | |
* sealed class RoomType { | |
* @Json(name = "c") class Public : RoomType() | |
* @Json(name = "d") class OneToOne: RoomType() | |
* class Custom(val rawType: String) : RoomType() | |
* } | |
* `</pre> | |
*/ | |
@Documented | |
@Retention(AnnotationRetention.RUNTIME) | |
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) | |
annotation class FallbackSealedClass(val name: String, val fieldName: String) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2018 Lucio Maciel, Rocket.Chat | |
* Copyright 2016 Serj Lotutovici | |
* Copyright (C) 2014 Square, 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. | |
*/ | |
import com.squareup.moshi.* | |
import java.io.IOException | |
import java.lang.reflect.Constructor | |
import java.lang.reflect.Field | |
import java.util.LinkedHashMap | |
/** | |
* [JsonAdapter] that fallbacks to a default class type declared in the sealed class annotated | |
* with [FallbackSealedClass]. | |
* | |
* | |
*/ | |
class FallbackSealedClassJsonAdapter<T>(private val classType: Class<T>, | |
fallback: String, | |
private val fieldName: String) : JsonAdapter<T>() { | |
private val fallbackConstant: Class<out T> | |
private val fallbackConstructor: Constructor<out T> | |
private val nameConstantMap: Map<String, Class<out T>> | |
private val nameStrings: Array<String?> | |
private val fallbackConstructorField: Field | |
init { | |
try { | |
var fallbackConstantIndex = -1 | |
val classes = classType.classes | |
val nameMap = LinkedHashMap<String, Class<out T>>() | |
nameStrings = arrayOfNulls(classes.size) | |
for (index in classes.indices) { | |
val clazz = classes[index] | |
val annotation = clazz.getAnnotation(Json::class.java) | |
val name = annotation?.name ?: clazz.simpleName | |
nameMap[name] = clazz as Class<out T> | |
nameStrings[index] = name | |
if (fallback == clazz.simpleName) { | |
fallbackConstantIndex = index | |
} | |
} | |
if (fallbackConstantIndex != -1) { | |
fallbackConstant = classes[fallbackConstantIndex] as Class<out T> | |
fallbackConstructor = fallbackConstant.getConstructor(String::class.java) | |
fallbackConstructorField = fallbackConstant.getDeclaredField(fieldName) | |
} else { | |
throw NoSuchFieldException("Filed \"$fallback\" is not declared.") | |
} | |
nameConstantMap = nameMap.toMap() | |
} catch (e: NoSuchFieldException) { | |
throw AssertionError("Missing field in " + classType.name, e) | |
} catch (e: NoSuchMethodException) { | |
throw AssertionError("Missing constructor with \"String\" parameter") | |
} catch (e: SecurityException) { | |
throw AssertionError("Invalid permission for constructor") | |
} | |
} | |
@Throws(IOException::class) | |
override fun fromJson(reader: JsonReader): T? { | |
val name = reader.nextString() | |
val constant = nameConstantMap[name] | |
return constant?.newInstance() ?: fallbackConstructor.newInstance(name) | |
} | |
@Throws(IOException::class) | |
override fun toJson(writer: JsonWriter, value: T?) { | |
value?.let { | |
if (fallbackConstant.isInstance(value)) { | |
val accessible = fallbackConstructorField.isAccessible | |
fallbackConstructorField.isAccessible = true | |
writer.value(fallbackConstructorField.get(value) as String) | |
fallbackConstructorField.isAccessible = accessible | |
return | |
} | |
for (entry in nameConstantMap) { | |
if (entry.value.isInstance(value)) { | |
writer.value(entry.key) | |
return | |
} | |
} | |
} | |
} | |
override fun toString(): String { | |
return "JsonAdapter(" + classType.name + ").fallbackClass(" + fallbackConstant + ")" | |
} | |
companion object { | |
/** | |
* Builds an adapter that can process sealed classes annotated with [FallbackSealedClass]. | |
*/ | |
val ADAPTER_FACTORY: JsonAdapter.Factory = JsonAdapter.Factory { type, annotations, moshi -> | |
if (!annotations.isEmpty()) return@Factory null | |
val rawType = Types.getRawType(type) | |
val annotation = rawType.getAnnotation(FallbackSealedClass::class.java) ?: return@Factory null | |
return@Factory FallbackSealedClassJsonAdapter(rawType, annotation.name, annotation.fieldName) | |
.nullSafe() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.squareup.moshi.Moshi | |
import org.hamcrest.MatcherAssert.assertThat | |
import org.junit.Before | |
import org.junit.Test | |
import org.mockito.MockitoAnnotations | |
import org.hamcrest.CoreMatchers.`is` as isEqualTo | |
class FallbackSealedClassJsonAdapterTest { | |
lateinit var moshi: Moshi | |
@Before | |
fun setup() { | |
MockitoAnnotations.initMocks(this) | |
moshi = Moshi.Builder() | |
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY) | |
.build() | |
} | |
@Test(expected = AssertionError::class) | |
fun `should fail with missing fallback field`() { | |
val adapter = moshi.adapter<NoFallback>(NoFallback::class.java) | |
val value1 = adapter.fromJson(NO_FALLBACK_TEST1) | |
assert(value1?.value is NoFallbackClass.value1) | |
} | |
@Test(expected = AssertionError::class) | |
fun `should fail with missing Fallback constructor field`(){ | |
val adapter = moshi.adapter<MissingField>(MissingField::class.java) | |
val value1 = adapter.fromJson(MISSING_FIELD_TEST1) | |
assert(value1?.value is MissingFieldClass.value1) | |
} | |
@Test(expected = AssertionError::class) | |
fun `should fail with wrong Constructor field name`(){ | |
val adapter = moshi.adapter<InvalidFieldName>(InvalidFieldName::class.java) | |
val value1 = adapter.fromJson(INVALID_FIELD_TEST1) | |
assert(value1?.value is InvalidFieldNameClass.value1) | |
} | |
@Test | |
fun `should parse value1 value`() { | |
val adapter = moshi.adapter<ValidFallback>(ValidFallback::class.java) | |
val value = adapter.fromJson(VALID_TEST1) | |
assert(value?.value is ValidFallbackClass.value1) | |
} | |
@Test | |
fun `should parse value2 value`() { | |
val adapter = moshi.adapter<ValidFallback>(ValidFallback::class.java) | |
val value = adapter.fromJson(VALID_TEST2) | |
assert(value?.value is ValidFallbackClass.value2) | |
} | |
@Test | |
fun `should return Fallback(value)`() { | |
val adapter = moshi.adapter<ValidFallback>(ValidFallback::class.java) | |
val value = adapter.fromJson(VALID_TEST3)!! | |
assert(value.value is ValidFallbackClass.Fallback) | |
assertThat((value.value as ValidFallbackClass.Fallback).field, isEqualTo("value")) | |
} | |
@Test | |
fun `should return Fallback(test)`() { | |
val adapter = moshi.adapter<ValidFallback>(ValidFallback::class.java) | |
val value = adapter.fromJson(VALID_TEST4)!! | |
assert(value.value is ValidFallbackClass.Fallback) | |
assertThat((value.value as ValidFallbackClass.Fallback).field, isEqualTo("test")) | |
} | |
@Test | |
fun `should parse empty value`() { | |
val adapter = moshi.adapter<ValidFallback>(ValidFallback::class.java) | |
val value = adapter.fromJson(VALID_TEST5)!! | |
assert(value.value is ValidFallbackClass.Fallback) | |
assertThat((value.value as ValidFallbackClass.Fallback).field, isEqualTo("")) | |
} | |
} | |
@FallbackSealedClass(name = "Fallback", fieldName = "field") | |
sealed class NoFallbackClass { | |
class value1 : NoFallbackClass() | |
class value2 : NoFallbackClass() | |
} | |
data class NoFallback(val value: NoFallbackClass) | |
const val NO_FALLBACK_TEST1 = "{\"value\":\"value1\"}" | |
@FallbackSealedClass(name = "Fallback", fieldName = "field") | |
sealed class MissingFieldClass { | |
class value1 : MissingFieldClass() | |
class value2 : MissingFieldClass() | |
class Fallback : MissingFieldClass() | |
} | |
data class MissingField(val value: MissingFieldClass) | |
const val MISSING_FIELD_TEST1 = "{\"value\":\"value1\"}" | |
@FallbackSealedClass(name = "Fallback", fieldName = "field") | |
sealed class InvalidFieldNameClass { | |
class value1 : InvalidFieldNameClass() | |
class value2 : InvalidFieldNameClass() | |
class Fallback(val value: String) : InvalidFieldNameClass() | |
} | |
data class InvalidFieldName(val value: InvalidFieldNameClass) | |
const val INVALID_FIELD_TEST1 = "{\"value\":\"value1\"}" | |
@FallbackSealedClass(name = "Fallback", fieldName = "field") | |
sealed class ValidFallbackClass { | |
class value1 : ValidFallbackClass() | |
class value2 : ValidFallbackClass() | |
class Fallback(val field: String) : ValidFallbackClass() | |
} | |
data class ValidFallback(val value: ValidFallbackClass) | |
const val VALID_TEST1 = "{\"value\":\"value1\"}" | |
const val VALID_TEST2 = "{\"value\":\"value2\"}" | |
const val VALID_TEST3 = "{\"value\":\"value\"}" | |
const val VALID_TEST4 = "{\"value\":\"test\"}" | |
const val VALID_TEST5 = "{\"value\":\"\"}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment