Repository: Karumi/KotlinSnapshot (kotlin)

Issue:
Can't serialize object with List attribute
### Expected behaviour
It should serialize and generate the snapshot file

### Actual behaviour
An error is thrown:
`com.google.gson.JsonParseException: cannot serialize java.util.Arrays$ArrayList; did you forget to register a subtype?`

### Steps to reproduce
call  `matchwithSnapshot()` on an object that has a list attribute
E.g.
```
data class TestObject(val list: List<String> = listOf("a", "b"))

@Test
fun test() {
  TestObject().matchWithSnapshot()
}
```

### Version of the library
2.2.2

### Gradle version

Pull request:
Fixes #68

When declaring a class using a collection supertype the parser wasn't able to find the delegate we should use to parse the implementation detail passed on runtime. The class declared with a generic List wasn't ready to parse ArrayList or LinkedList instances. However, if a class was declared with the specific implementation type it worked like a charm. This PR fixes this issue 😃

Patch:
diff --git a/core/build.gradle b/core/build.gradle
index 93e0927..390fda8 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -33,7 +33,7 @@ dependencies {
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
     implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
     implementation "org.bitbucket.cowwoc.diff-match-patch:diff-match-patch:1.0"
-    implementation "com.google.code.gson:gson:2.8.5"
+    implementation "com.google.code.gson:gson:2.8.6"
     implementation  group: 'junit', name: 'junit', version: '4.12'
     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.2.0'
     testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
diff --git a/core/src/main/java/com/karumi/kotlinsnapshot/core/serializers/RuntimeClassNameTypeAdapterFactory.java b/core/src/main/java/com/karumi/kotlinsnapshot/core/serializers/RuntimeClassNameTypeAdapterFactory.java
index 69e7c79..ab842bd 100644
--- a/core/src/main/java/com/karumi/kotlinsnapshot/core/serializers/RuntimeClassNameTypeAdapterFactory.java
+++ b/core/src/main/java/com/karumi/kotlinsnapshot/core/serializers/RuntimeClassNameTypeAdapterFactory.java
@@ -171,12 +171,13 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
                 = new LinkedHashMap<>();
 
 
-        if (Object.class.isAssignableFrom(type.getRawType())) {
+        Class<? super R> rawType = type.getRawType();
+        if (Object.class.isAssignableFrom(rawType)) {
             TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
             labelToDelegate.put("__class__", delegate);
-            subtypeToDelegate.put(type.getRawType(), delegate);
+            subtypeToDelegate.put(rawType, delegate);
         }
-
+        TypeAdapterFactory factory = this;
 
         return new TypeAdapter<R>() {
             @Override
@@ -204,9 +205,19 @@ public void write(JsonWriter out, R value) throws IOException {
                 String label = srcType.getSimpleName();
                 @SuppressWarnings("unchecked") // registration requires that subtype extends T
                         TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
+                // Types declared using the interface but provided in runtime using a specific implementation may not be
+                // declared on the delegates we saved before so we need to add it to the delegates' list using the
+                // implementation type provided on runtime and not
+                if (delegate == null) {
+                    TypeToken<?> typeToken = TypeToken.get(srcType);
+                    TypeAdapter<?> implementationDelegate = gson.getDelegateAdapter(factory, typeToken);
+                    labelToDelegate.put("__class__", implementationDelegate);
+                    subtypeToDelegate.put(srcType, implementationDelegate);
+                    delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
+                }
+
                 if (delegate == null) {
-                    throw new JsonParseException("cannot serialize " + srcType.getName()
-                            + "; did you forget to register a subtype?");
+                    throw new JsonParseException("cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
                 }
                 JsonElement jsonTree = delegate.toJsonTree(value);
                 if (jsonTree.isJsonPrimitive() || jsonTree.isJsonArray()) {


Test patch:
diff --git a/core/__snapshot__/com.karumi.kotlinsnapshot.core.ArrayListTest_should serialize any class with a non empty list inside.snap b/core/__snapshot__/com.karumi.kotlinsnapshot.core.ArrayListTest_should serialize any class with a non empty list inside.snap
new file mode 100644
index 0000000..7f866ef
--- /dev/null
+++ b/core/__snapshot__/com.karumi.kotlinsnapshot.core.ArrayListTest_should serialize any class with a non empty list inside.snap
@@ -0,0 +1,7 @@
+{
+  "__class__": "TestObject",
+  "list": [
+    "item1",
+    "item2"
+  ]
+}
\ No newline at end of file
diff --git a/core/__snapshot__/com.karumi.kotlinsnapshot.core.ArrayListTest_should serialize any class with an empty list inside.snap b/core/__snapshot__/com.karumi.kotlinsnapshot.core.ArrayListTest_should serialize any class with an empty list inside.snap
new file mode 100644
index 0000000..f5679ea
--- /dev/null
+++ b/core/__snapshot__/com.karumi.kotlinsnapshot.core.ArrayListTest_should serialize any class with an empty list inside.snap
@@ -0,0 +1,4 @@
+{
+  "__class__": "TestObject",
+  "list": []
+}
\ No newline at end of file
diff --git a/core/src/test/kotlin/com/karumi/kotlinsnapshot/core/ArrayListTest.kt b/core/src/test/kotlin/com/karumi/kotlinsnapshot/core/ArrayListTest.kt
new file mode 100644
index 0000000..86af9a5
--- /dev/null
+++ b/core/src/test/kotlin/com/karumi/kotlinsnapshot/core/ArrayListTest.kt
@@ -0,0 +1,23 @@
+package com.karumi.kotlinsnapshot.core
+
+import com.karumi.kotlinsnapshot.matchWithSnapshot
+import org.junit.Test
+
+class ArrayListTest {
+
+    @Test
+    fun `should serialize any class with an empty list inside`() {
+        val input = TestObject(emptyList())
+
+        input.matchWithSnapshot()
+    }
+
+    @Test
+    fun `should serialize any class with a non empty list inside`() {
+        val input = TestObject(listOf("item1", "item2"))
+
+        input.matchWithSnapshot()
+    }
+
+    data class TestObject(val list: List<String>)
+}