Skip to content

Commit

Permalink
feat(): Automatic resolver parameter typing (#10)
Browse files Browse the repository at this point in the history
* Pass the Resolver parameters as receiver
* Pass the type of the source in the `type()` function
  • Loading branch information
mbiamont authored Oct 8, 2023
1 parent 1521859 commit d74905d
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 94 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = "com.arianegraphql"
version = "0.1.0"
version = "0.2.0"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ dependencies {

implementation(project(":server"))
implementation(project(":server-ktor"))
implementation("io.ktor:ktor-server-cio:1.6.3")
implementation("io.ktor:ktor-server-cio:2.3.4")
}
84 changes: 9 additions & 75 deletions sample/src/main/kotlin/com/arianegraphql/sample/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,123 +3,57 @@ package com.arianegraphql.sample
import com.arianegraphql.ktx.loadSchema
import com.arianegraphql.sample.model.Director
import com.arianegraphql.sample.model.Movie
import com.arianegraphql.server.graphql.GraphQLRequest
import com.arianegraphql.server.graphql.GraphQLResponse
import com.arianegraphql.server.ktor.dsl.arianeServer
import com.arianegraphql.server.ktor.launch
import com.arianegraphql.server.listener.RequestListener
import com.arianegraphql.server.listener.ServerListener
import com.arianegraphql.server.listener.SubscriptionListener
import com.arianegraphql.server.request.HttpRequest
import graphql.ExecutionInput
import graphql.GraphQLContext
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.server.engine.*
import kotlinx.coroutines.flow.*

val movies = mutableListOf<Movie>()
val onMovieAdded = MutableSharedFlow<Movie>()

@Suppress("UNUSED_ANONYMOUS_PARAMETER")
fun main() {
populateMovies()

arianeServer {
schema = loadSchema("schema.graphql")
port = 3000

requestListener = object : RequestListener {

override fun onReceived(httpRequest: HttpRequest) {
//Called when a request has been received.
}

override fun onParsed(graphQLRequest: GraphQLRequest) {
//Called when a request has been successfully parsed.
}

override fun onValidated(executionInput: ExecutionInput) {
//Called when a GraphQL request has been validated.
}

override fun onExecuted(graphQLResponse: GraphQLResponse) {
//Called when a GraphQL request has been executed.
}

override fun onResponded() {
//Called when a HTTP request has been responded.
}

override fun onError(e: Throwable) {
//Called when an error occurred during the request.
}

}

serverListener = object : ServerListener {

override fun onStart(host: String, port: Int, path: String) {
println("Server ready 🚀")
println("Listening at http://localhost:$port$path")
}

override fun onStop() {
println("Server stop. Bye. 🌜")
}
}

subscriptionListener = object : SubscriptionListener {

override fun onNewConnection(sessionId: String) {
println("[$sessionId] onNewConnection()")
}

override fun onConnected(sessionId: String, context: GraphQLContext) {
println("[$sessionId] onConnected()")
}

override fun onStartSubscription(sessionId: String, context: GraphQLContext, operationId: String, graphQLRequest: GraphQLRequest) {
println("[$sessionId] onStartSubscription($operationId, ${graphQLRequest.operationName})")
}

override fun onStopSubscription(sessionId: String, context: GraphQLContext, operationId: String) {
println("[$sessionId] onStopSubscription($operationId)")
}

override fun onCloseConnection(sessionId: String, context: GraphQLContext) {
println("[$sessionId] onCloseConnection()")
}
}

resolvers {
Query {
resolve("movies") { _, _: Any?, _: GraphQLContext, _ ->
resolve("movies") {
movies
}

resolve("directors") { _, _: Any?, _: GraphQLContext, _ ->
resolve("directors") {
movies.map { it.director }.distinctBy { it.name }
}

resolve("movie") { args, _: Any?, _: GraphQLContext, _ ->
movies.find { it.title == args["title"] }
resolve("movie") {
movies.find { it.title == arguments["title"] }
}
}

Mutation {
resolve("addMovie") { args, parent: Any?, context: GraphQLContext, info ->
val movie = Movie(args["title"], Director(args["director"]))
resolve("addMovie") {
val movie = Movie(arguments["title"], Director(arguments["director"]))
movies.add(movie)

onMovieAdded.emit(movie)
movie
}
}

type("Director") {
resolve("movies") { args: Any, parent: Director, context: GraphQLContext, info ->
movies.filter { it.director.name == parent.name }
type<Director>("Director") {
resolve("movies") {
movies.filter { it.director.name == source.name }
}
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/main/kotlin/com/arianegraphql/ktx/Resolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ interface Resolver<S> {
}

@JvmInline value class FunctionalResolver<S>(
private val lambda: suspend (arguments: Argument, source: S, context: GraphQLContext, info: Info) -> Any?
private val lambda: suspend (parameters: ResolverParameters<S>) -> Any?
) : Resolver<S> {

override suspend fun resolve(
arguments: Argument,
source: S,
context: GraphQLContext,
info: Info
) = lambda(arguments, source, context, info)
) = lambda(ResolverParameters(arguments, source, context, info))
}

internal fun <S> Resolver<S>.toDataFetcher(): DataFetcher<Any?> = DataFetcher { env ->
Expand Down
10 changes: 10 additions & 0 deletions server/src/main/kotlin/com/arianegraphql/ktx/ResolverParameters.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.arianegraphql.ktx

import graphql.GraphQLContext

data class ResolverParameters<S>(
val arguments: Argument,
val source: S,
val context: GraphQLContext,
val info: Info,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import graphql.schema.idl.TypeRuntimeWiring
class RootResolverBuilder {
private val rootResolver = mutableListOf<TypeRuntimeWiring.Builder>()

fun type(typeName: String, builder: TypeResolverBuilder.() -> Unit) {
rootResolver.add(TypeResolverBuilder().apply(builder).build(typeName))
fun <S> type(typeName: String, builder: TypeResolverBuilder<S>.() -> Unit) {
rootResolver.add(TypeResolverBuilder<S>().apply(builder).build(typeName))
}

fun Mutation(builder: TypeResolverBuilder.() -> Unit) = type(TYPE_MUTATION_NAME, builder)
fun Mutation(builder: TypeResolverBuilder<GraphQLTypes.Mutation>.() -> Unit) = type(TYPE_MUTATION_NAME, builder)

fun Query(builder: TypeResolverBuilder.() -> Unit) = type(TYPE_QUERY_NAME, builder)
fun Query(builder: TypeResolverBuilder<GraphQLTypes.Query>.() -> Unit) = type(TYPE_QUERY_NAME, builder)

fun Subscription(builder: SubscriptionTypeResolverBuilder.() -> Unit) {
rootResolver.add(SubscriptionTypeResolverBuilder().apply(builder).build(TYPE_SUBSCRIPTION_NAME))
Expand All @@ -23,4 +23,12 @@ class RootResolverBuilder {

const val TYPE_MUTATION_NAME = "Mutation"
const val TYPE_QUERY_NAME = "Query"
const val TYPE_SUBSCRIPTION_NAME = "Subscription"
const val TYPE_SUBSCRIPTION_NAME = "Subscription"

object GraphQLTypes {
object Mutation

object Query

object Subscription
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ import kotlinx.coroutines.reactive.asPublisher
import org.reactivestreams.Publisher

@GraphQLSchemaDslMarker
class SubscriptionTypeResolverBuilder : TypeResolverBuilder() {
class SubscriptionTypeResolverBuilder : TypeResolverBuilder<GraphQLTypes.Subscription>() {

fun <T> resolve(field: String, publisher: Publisher<T>) = resolve(field) { _, _: Any?, _: Any?, _ -> publisher }
fun <T> resolve(field: String, publisher: Publisher<T>) = resolve(field) { publisher }

fun <T : Any> resolve(field: String, flow: Flow<T>) = resolve(field, flow.asPublisher())

fun <T : Any, S> resolve(field: String, flow: Flow<T>, predicate: SubscriptionFilter<T, S>) =
resolve(field) { arguments: Argument, source: S, context: GraphQLContext, info: Info ->
fun <T : Any> resolve(field: String, flow: Flow<T>, predicate: SubscriptionFilter<T, GraphQLTypes.Subscription>) =
resolve(field) {
flow.filter {
predicate.test(arguments, source, context, info, it)
}.asPublisher()
}

fun <T : Any, S, C> resolve(
fun <T : Any> resolve(
field: String,
flow: Flow<T>,
predicate: suspend (arguments: Argument, source: S, context: GraphQLContext, info: Info, item: T) -> Boolean
predicate: suspend (arguments: Argument, source: GraphQLTypes.Subscription, context: GraphQLContext, info: Info, item: T) -> Boolean
) = resolve(field, flow, FunctionalSubscriptionFilter(predicate))
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package com.arianegraphql.ktx

import graphql.GraphQLContext
import graphql.schema.idl.TypeRuntimeWiring

@GraphQLSchemaDslMarker
open class TypeResolverBuilder {
open class TypeResolverBuilder<S> {

internal val typeResolver = mutableMapOf<String, Resolver<*>>()

fun <S> resolve(field: String, resolver: Resolver<S>) {
fun resolve(field: String, resolver: Resolver<S>) {
typeResolver[field] = resolver
}

fun <S> resolve(field: String, resolver: suspend (arguments: Argument, source: S, context: GraphQLContext, info: Info) -> Any?) {
fun resolve(field: String, resolver: suspend ResolverParameters<S>.() -> Any?) {
typeResolver[field] = FunctionalResolver(resolver)
}

Expand Down

0 comments on commit d74905d

Please sign in to comment.