Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce FakeResolver #4328

Merged
merged 7 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apollo-annotations/api/apollo-annotations.api
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public final class com/apollographql/apollo3/annotations/ApolloDeprecatedSince$V
public static final field v3_3_2 Lcom/apollographql/apollo3/annotations/ApolloDeprecatedSince$Version;
public static final field v3_3_3 Lcom/apollographql/apollo3/annotations/ApolloDeprecatedSince$Version;
public static final field v3_4_1 Lcom/apollographql/apollo3/annotations/ApolloDeprecatedSince$Version;
public static final field v3_5_1 Lcom/apollographql/apollo3/annotations/ApolloDeprecatedSince$Version;
public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo3/annotations/ApolloDeprecatedSince$Version;
public static fun values ()[Lcom/apollographql/apollo3/annotations/ApolloDeprecatedSince$Version;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ annotation class ApolloDeprecatedSince(val version: Version) {
v3_3_2,
v3_3_3,
v3_4_1,
v3_5_1
}
}
28 changes: 28 additions & 0 deletions apollo-api/api/apollo-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,15 @@ public final class com/apollographql/apollo3/api/CustomTypeValue$GraphQLString :
public fun <init> (Ljava/lang/String;)V
}

public final class com/apollographql/apollo3/api/DefaultFakeResolver : com/apollographql/apollo3/api/FakeResolver {
public fun <init> (Ljava/util/List;)V
public final fun getTypes ()Ljava/util/List;
public fun resolveLeaf (Lcom/apollographql/apollo3/api/FakeResolverContext;)Ljava/lang/Object;
public fun resolveListSize (Lcom/apollographql/apollo3/api/FakeResolverContext;)I
public fun resolveMaybeNull (Lcom/apollographql/apollo3/api/FakeResolverContext;)Z
public fun resolveTypename (Lcom/apollographql/apollo3/api/FakeResolverContext;)Ljava/lang/String;
}

public final class com/apollographql/apollo3/api/DefaultUpload : com/apollographql/apollo3/api/Upload {
public fun getContentLength ()J
public fun getContentType ()Ljava/lang/String;
Expand Down Expand Up @@ -491,6 +500,8 @@ public final class com/apollographql/apollo3/api/DeferredFragmentIdentifier {

public final class com/apollographql/apollo3/api/EnumType : com/apollographql/apollo3/api/CompiledNamedType {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/util/List;)V
public final fun getValues ()Ljava/util/List;
}

public final class com/apollographql/apollo3/api/Error {
Expand Down Expand Up @@ -579,6 +590,23 @@ public final class com/apollographql/apollo3/api/ExecutionOptions$Companion {
public static final field CAN_BE_BATCHED Ljava/lang/String;
}

public abstract interface class com/apollographql/apollo3/api/FakeResolver {
public abstract fun resolveLeaf (Lcom/apollographql/apollo3/api/FakeResolverContext;)Ljava/lang/Object;
public abstract fun resolveListSize (Lcom/apollographql/apollo3/api/FakeResolverContext;)I
public abstract fun resolveMaybeNull (Lcom/apollographql/apollo3/api/FakeResolverContext;)Z
public abstract fun resolveTypename (Lcom/apollographql/apollo3/api/FakeResolverContext;)Ljava/lang/String;
}

public final class com/apollographql/apollo3/api/FakeResolverContext {
public fun <init> (Ljava/util/List;Lcom/apollographql/apollo3/api/CompiledField;)V
public final fun getMergedField ()Lcom/apollographql/apollo3/api/CompiledField;
public final fun getPath ()Ljava/util/List;
}

public final class com/apollographql/apollo3/api/FakeResolverKt {
public static final fun buildData (Lcom/apollographql/apollo3/api/Adapter;Ljava/util/List;Ljava/lang/String;Ljava/util/Map;Lcom/apollographql/apollo3/api/FakeResolver;)Ljava/lang/Object;
}

public final class com/apollographql/apollo3/api/FileUpload {
public static final fun content (Lcom/apollographql/apollo3/api/DefaultUpload$Builder;Ljava/io/File;)Lcom/apollographql/apollo3/api/DefaultUpload$Builder;
public static final fun create (Ljava/lang/String;Ljava/lang/String;)Lcom/apollographql/apollo3/api/Upload;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,12 @@ class InputObjectType(

class EnumType(
name: String,
) : CompiledNamedType(name)
val values: List<String>
) : CompiledNamedType(name) {
@Deprecated("Use the primary constructor instead")
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v3_5_1)
constructor(name: String): this(name, emptyList())
}

class ScalarType(
name: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ class BuilderProperty<T>(val adapter: Adapter<T>) {
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package com.apollographql.apollo3.api

import com.apollographql.apollo3.api.json.MapJsonReader

/**
* @property path the path to the element being resolved. [path] is a list of [String] or [Int]
* @property mergedField the field being resolved
*/
class FakeResolverContext(
val path: List<Any>,
val mergedField: CompiledField,
)

/**
* Provides fakes values for Data builders
*/
interface FakeResolver {
/**
* Resolves a leaf (scalar or enum) type. Note that because of list and not-nullable
* types, the type of `context.mergedField` is not always the leaf type.
* You can get the type of the leaf type with:
*
* ```
* context.mergedField.type.leafType()
* ```
*
* @return a kotlin value representing the value at path `context.path`. Possible values include
* - Boolean
* - Int
* - Double
* - Strings
* - Custom scalar targets (see below)
*
* Custom scalars: for custom scalars whose adapter is registered at build time, [resolveLeaf] **must**
* return a Json representation of the scalar (using Map, List, Boolean, Int, Double, String).
* Target instances such as `Date`, `BigDecimal`, etc... are not supported because they would require [CustomScalarAdapters] to decode.
*/
fun resolveLeaf(context: FakeResolverContext): Any

/**
* Resolves the size of a list. Note that lists might be nested. You can use `context.path` to get
* the current nesting depth
*/
fun resolveListSize(context: FakeResolverContext): Int

/**
* @return true if the current value should return null
*/
fun resolveMaybeNull(context: FakeResolverContext): Boolean

/**
* @return a concrete type that implements the type in `context.mergedField.type.leafType()`
*/
fun resolveTypename(context: FakeResolverContext): String
}

private fun collect(selections: List<CompiledSelection>, typename: String): List<CompiledField> {
return selections.flatMap { compiledSelection ->
when (compiledSelection) {
is CompiledField -> {
listOf(compiledSelection)
}

is CompiledFragment -> {
if (typename in compiledSelection.possibleTypes) {
collect(compiledSelection.selections, typename)
} else {
emptyList()
}
}
}
}
}

private fun collectAndMerge(selections: List<CompiledSelection>, typename: String): List<CompiledField> {
/**
* This doesn't check the condition and will therefore overfetch
*/
return collect(selections, typename).groupBy { it.responseName }.values.map { fields ->
val first = fields.first()

CompiledField.Builder(first.name, first.type)
.alias(first.alias)
.selections(fields.flatMap { field -> field.selections })
.build()
}
}

/**
* @param selections: the selections of the operation to fake
* @param typename: the type of the object currently being resolved. Always an object type
* @param base:
*/
private fun buildFakeObject(
selections: List<CompiledSelection>,
typename: String,
base: Map<String, Any?>,
resolver: FakeResolver,
): Map<String, Any?> {
@Suppress("UNCHECKED_CAST")
return buildFieldOfType(
emptyList(),
CompiledField.Builder("data", CompiledNotNullType(ObjectType.Builder(typename).build()))
.selections(selections)
.build(),
resolver,
Optional.Present(base),
CompiledNotNullType(ObjectType.Builder(typename).build())
) as Map<String, Any?>
}

private fun buildField(
path: List<Any>,
mergedField: CompiledField,
resolver: FakeResolver,
parent: Map<String, Any?>,
): Any? {
return buildFieldOfType(path, mergedField, resolver, parent.getOrAbsent(mergedField.responseName), mergedField.type)
}

private fun Map<String, Any?>.getOrAbsent(key: String) = if (containsKey(key)) {
Optional.Present(get(key))
} else {
Optional.Absent
}

private fun buildFieldOfType(
path: List<Any>,
mergedField: CompiledField,
resolver: FakeResolver,
value: Optional<Any?>,
type: CompiledType,
): Any? {
if (type !is CompiledNotNullType) {
return if (value is Optional.Present) {
if (value.value == null) {
null
} else {
buildFieldOfType(path, mergedField, resolver, value, CompiledNotNullType(type))
}
} else {
if (resolver.resolveMaybeNull(FakeResolverContext(path, mergedField))) {
null
} else {
buildFieldOfType(path, mergedField, resolver, value, CompiledNotNullType(type))
}
}
}

return buildFieldOfNonNullType(path, mergedField, resolver, value, type.ofType)
}

private fun buildFieldOfNonNullType(
path: List<Any>,
mergedField: CompiledField,
resolver: FakeResolver,
value: Optional<Any?>,
type: CompiledType,
): Any? {
return when (type) {
is CompiledListType -> {
if (value is Optional.Present) {
val list = (value.value as? List<Any?>) ?: error("")
list.mapIndexed { index, item ->
buildFieldOfType(path + index, mergedField, resolver, Optional.Present(item), type.ofType)
}
} else {
0.until(resolver.resolveListSize(FakeResolverContext(path, mergedField))).map {
buildFieldOfType(path + it, mergedField, resolver, Optional.Absent, type.ofType)
}
}
}

is CompiledNamedType -> {
if (value is Optional.Present) {
if (mergedField.selections.isNotEmpty()) {
@Suppress("UNCHECKED_CAST")
val map = (value.value as? Map<String, Any?>) ?: error("")
val typename = (map["__typename"] as? String) ?: error("")

collectAndMerge(mergedField.selections, typename).associate {
it.responseName to buildField(path + it.responseName, it, resolver, map)
}
} else {
value.value
}
} else {
if (mergedField.selections.isNotEmpty()) {
val typename = resolver.resolveTypename(FakeResolverContext(path, mergedField))
val map = mapOf("__typename" to typename)

collectAndMerge(mergedField.selections, typename).associate {
it.responseName to buildField(path + it.responseName, it, resolver, map)
}
} else {
resolver.resolveLeaf(FakeResolverContext(path, mergedField))
}
}
}

is CompiledNotNullType -> error("")
}
}

class DefaultFakeResolver(val types: List<CompiledNamedType>) : FakeResolver {
private var currentInt = 0
private var currentFloat = 0.0
private var currentBoolean = false

private var impl = mutableMapOf<String, Int>()

override fun resolveLeaf(context: FakeResolverContext): Any {
return when (val name = context.mergedField.type.leafType().name) {
"Int" -> currentInt++
"Float" -> currentFloat++
"Boolean" -> (!currentBoolean).also { currentBoolean = it }
"String" -> {
val index = context.path.indexOfLast { it is String }
context.path.subList(index, context.path.size).joinToString { it.toPathComponent() }
}

"ID" -> context.path.joinToString { it.toString() }
else -> {
val type = (types.find { it.name == name } as? EnumType) ?: error("Don't know how to instantiate leaf $name")
val index = impl.getOrElse(name) { 0 }

impl[name] = index + 1

type.values[index % type.values.size]
}
}
}

private fun Any.toPathComponent(): String = when (this) {
is Int -> "[$this]"
else -> toString()
}

override fun resolveListSize(context: FakeResolverContext): Int {
return 3
}

override fun resolveMaybeNull(context: FakeResolverContext): Boolean {
return false
}

override fun resolveTypename(context: FakeResolverContext): String {
val leafType = context.mergedField.type.leafType()
val name = leafType.name
val index = impl.getOrElse(name) { 0 }

impl[name] = index + 1

// XXX: Cache this computation
val possibleTypes = possibleTypes(types, leafType)
return possibleTypes[index % possibleTypes.size].name
}
}

fun <T> buildData(
adapter: Adapter<T>,
selections: List<CompiledSelection>,
typename: String,
map: Map<String, Any?>,
resolver: FakeResolver,
): T {
return adapter.obj(false).fromJson(
MapJsonReader(
buildFakeObject(selections, typename, map, resolver)
),
CustomScalarAdapters.Unsafe
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ private fun possibleTypesInternal(allTypes: List<CompiledType>, type: CompiledNa
}
}

/**
* Returns all objects that implement [type]
*/
fun possibleTypes(allTypes: List<CompiledType>, type: CompiledNamedType): List<ObjectType> {
return possibleTypesInternal(allTypes, type).distinctBy { it.name }.sortedBy { it.name }
}
Loading