apollo-kotlin icon indicating copy to clipboard operation
apollo-kotlin copied to clipboard

Data builders

Open martinbonnin opened this issue 3 years ago • 1 comments

When working with Jetpack Compose @Preview, faking data is useful in the main source set while currently, the test builders are only generated for the test source set by default. This can be overriden but it's not straightforward. Also because the current test builders are responseBased, they generate a fair amount of code, not to mention that specific code is generated for each and every operation.

Instead, we could generate schemaBased test builders only once and add them to the main source set. For a query like this:

query HeroName {
  hero {
    name
  }
}

generate a fake model like this:

// pseudo code, not sure this can work exactly like this
val data = buildFakeData<HeroName> {
  hero = humanHero {
    name = "Luke"
    // Here it's not really type safe but that's certainly OK for fake models
    height = 180
  }
}

We could even enable strict mode so that errors are caught at runtime: see https://github.com/apollographql/apollo-kotlin/issues/3344

Todo:

  • [x] custom scalars support
  • [x] default values
  • [ ] use in our own tests
  • [ ] add a kotlin-faker resolver
  • [ ] java codegen
  • [ ] strict mode
  • [ ] update doc
  • [ ] (maybe) cleanup the IrType2 stuff

martinbonnin avatar Apr 11 '22 13:04 martinbonnin

Current proposal for generated code:

class HeroQuery {
  class Data(val hero: Hero) {

  }
  class Hero(val name: String)

  companion object {
    fun Data(block: QueryBuilder.() -> Unit): Data {
      // Code goes here
    }
  }  
}

class AliasedProperty(val alias: String, val map: MutableMap<String, Any?>) {
  infix fun to(value: Any) {
    map[alias] = value
  }
}


@Suppress("PropertyName")
abstract class ObjectBuilder {
  protected val __map = mutableMapOf<String, Any?>()
  
  var __typename: String by __map

  fun alias(alias: String): AliasedProperty {
    return AliasedProperty(alias, __map)
  }
}

class QueryBuilder: ObjectBuilder() {
  var hero: Character? by __map

  fun build(): Query {
    return Query(__map)
  }
}

fun buildQuery(block: QueryBuilder.() -> Unit): Query {
  val builder = QueryBuilder()
  builder.__typename = "Query"

  builder.block()

  return builder.build()
}

class Query(map: Map<String, Any?>): Map<String, Any?> by map

class HumanBuilder: ObjectBuilder()  {
  var name: String? by __map

  var height: Double? by __map

  fun build(): Human {
    return Human(__map)
  }
}
fun buildHuman(block: HumanBuilder.() -> Unit): Human {
  val builder = HumanBuilder()
  builder.__typename = "Human"

  builder.block()

  return builder.build()
}

class DroidBuilder: ObjectBuilder()  {
  var name: String? by __map

  var primaryFunction: String? by __map

  fun build(): Droid {
    return Droid(__map)
  }
}

fun buildDroid(block: DroidBuilder.() -> Unit): Droid {
  val builder = DroidBuilder()
  builder.__typename = "Droid"

  builder.block()

  return builder.build()
}



interface Character
class Human(map: Map<String, Any?>): Character, Map<String, Any?> by map
class Droid(map: Map<String, Any?>): Character, Map<String, Any?> by map

HeroQuery.Data {
  hero = buildHuman {
    name = "Luke"
    height = 1.7
  }

  alias("hero1") to buildDroid {
    name = "c3pO"
    primaryFunction = "translation"
  }
}

martinbonnin avatar Aug 05 '22 08:08 martinbonnin

Will be available in 3.6.0

martinbonnin avatar Aug 31 '22 11:08 martinbonnin