数据构建器
数据构建器当前正处于 Apollo Kotlin 的 experimental 阶段。如果您对它们有反馈,请通过 GitHub issue或Kotlin Slack 社区联系我们。
Apollo Kotlin 为您的 操作 生成模型和解析器,这些解析器可以从您的网络响应中创建这些模型的实例。但是,在某些情况下,例如在测试或其他场景中,手动使用已知值创建模型可能很有用。
这样做并不像看起来那么简单,尤其是当使用了 片段 时。operationBased
模型需要实例化每一个 片段,并为每个复合类型选择一个合适的 __typename
。
数据构建器通过提供与 Json 文档 结构相匹配的构建器,使这一过程更为简便。
启用数据构建器
要启用数据构建器,将 generateDataBuilders
选项设置为 true
:
apollo {service("service") {// ...// Enable data builder generationgenerateDataBuilders.set(true)}}
这是为您架构中的每个复合类型生成的构建器以及每个操作的帮助函数Data {}
。
示例用法
假设我们正在构建一个测试,该测试使用以下查询的模拟结果:
query HeroForEpisode($ep: Episode!) {hero(episode: $ep) {firstNamelastNameageship {modelspeed}friends {firstNamelastName}... on Droid {primaryFunction}... on Human {height}}}
以下是我们可以使用对应数据构建器对该模拟结果的用法
@Testfun test() {val data = HeroForEpisodeQuery.Data {// Set values for particular fields of the queryhero = buildHuman {firstName = "John"age = 42friends = listOf(buildHuman {firstName = "Jane"},buildHuman {lastName = "Doe"})ship = buildStarship {model = "X-Wing"}}}assertEquals("John", data.hero.firstName)assertEquals(42, data.hero.age)}
在这个例子中,hero
字段是一个具有指定值的Human
对象(firstName
和age
)。lastName
和height
的值会自动使用模拟值填充。ship的速度
、第一位朋友的姓氏和第二位朋友的名字的值也将自动填充。
您可以用buildHuman
代替上面的buildDroid
来创建一个Droid
对象。
别名
由于数据构建器是基于架构的,并且别名是在您的查询中定义的,所以代码生成无法为它们生成构建器字段。相反,您需要明确指定它们。
给定一个Query,如下所示:
query GetHeroes {luke: hero(id: "1002") {name}leia: hero(id: "1003") {name}}
您可以这样生成一个假数据模型
val data = GetHeroes.Data {this["luke"] = buildHumanHero {name = "Luke"}this["leia"] = buildHumanHero {name = "Leia"}}
@skip
和@include
指令
默认情况下,数据构建器与您的架构中定义的类型匹配。如果字段是非空的,您必须提供值或让默认解析器提供值。这对于@skip
和@include
指令是一个问题,其中字段可能是非空的,甚至是可选的。为了处理这种情况,使用与别名相同的语法,并设置值为Optional.Absent
。
query Skip($skip: Boolean!) {nonNullableInt @skip(if: $skip)}
您可以这样生成一个假数据模型
val data = SkipQuery.Data {this["nonNullableInt"] = Optional.Absent}assertNull(data.nonNullableInt)
配置默认字段值
要为字段设置默认值,数据构建者使用FakeResolver
接口的实现。默认情况下,它们使用DefaultFakeResolver
的实例。
默认的DefaultFakeResolver
为每个String
字段赋予字段的名称作为默认值,并为Int
字段增加计数。它为其他类型定义了类似的默认行为。
您可以创建自己的自定义 FakeResolver
实现(可以选择先委托给DefaultFakeResolver
以获得快速入门)。然后,像这样将实现作为参数传递给Data
函数:
// A FakeResolver implementation that assigns -1 to all Int fieldsclass MyFakeResolver : FakeResolver {private val delegate = DefaultFakeResolver(__Schema.all)override fun resolveLeaf(context: FakeResolverContext): Any {return when (context.mergedField.type.leafType().name) {"Int" -> -1 // Always use -1 for Intelse -> delegate.resolveLeaf(context)}}override fun resolveListSize(context: FakeResolverContext): Int {// Delegate to the default behaviourreturn delegate.resolveListSize(context)}override fun resolveMaybeNull(context: FakeResolverContext): Boolean {// Neverreturn false}override fun resolveTypename(context: FakeResolverContext): String {// Delegate to the default behaviourreturn delegate.resolveTypename(context)}}@Testfun test() {val data = HeroForEpisodeQuery.Data(resolver = MyFakeResolver()) {hero = buildHuman {firstName = "John"}}// Unspecified Int field is -1assertEquals(-1, data.hero.age)}
使用代码片段
由于片段片段可以定义在接口和联合体中,因此您需要明确您想要建模的具体类型。例如,如果您有一个Animal
接口,请使用以下方式创建一个Lion
片段数据:
val data = AnimalDetailsImpl.Data(Lion) {// you can access roar hereroar = "Grrrrr"}
有时,您可能想测试服务端上定义的未知于客户端的新类型。为了做到这一点,请使用抽象类型(Animal
在此处)作为Data
构造函数的第一参数。
在这些情况下,您需要显式指定返回的对象类型的__typename
:
val data = AnimalDetailsImpl.Data(Animal) {// the client doesn't know about this type yet, and you need to specify it explicitly__typename = "Brontaroc"species = "alien"}