Skip to content

Commit

Permalink
Prepare changelog and doc for next version
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Bonnin <martin@mbonnin.net>
  • Loading branch information
BoD and martinbonnin committed Aug 30, 2022
1 parent efc90e4 commit 9661fe0
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 4 deletions.
131 changes: 131 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,137 @@
Change Log
==========

# Version 3.6.0

_2022-08-30_

This version brings initial support for `@defer` as well as data builders.

## 💙️ External contributors

Many thanks to @engdorm, @Goooler, @pt2121 and @StylianosGakis for their contributions!

## ✨️ [new] `@defer` support

`@defer` is [stage 1 GraphQL spec proposal](https://github.com/graphql/graphql-spec/blob/main/CONTRIBUTING.md#stage-1-proposal) to allow incremental delivery of response payloads.

`@defer` allows you to mark a fragment as deferrable, meaning it can be omitted in the initial response and delivered as a subsequent payload. This improves latency for all fields that are not in that fragment. You can read more about `@defer` in the [RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md) and contribute/ask question in the [`@defer` working group](https://github.com/robrichard/defer-stream-wg).

Apollo Kotlin supports `@defer` by default and will deliver the successive payloads as `Flow` items. Given the below query:

```graphql
query GetComputer {
computer {
__typename
id
...ComputerFields @defer
}
}

fragment ComputerFields on Computer {
cpu
year
screen {
resolution
}
}
```

And the following server payloads:

**payload 1**:
```json
{
"data": {
"computer": {
"__typename": "Computer",
"id": "Computer1"
}
},
"hasNext": true
}
```

**payload 2**:
```json
{
"incremental": [
{
"data": {
"cpu": "386",
"year": 1993,
"screen": {
"resolution": "640x480"
}
},
"path": [
"computer",
]
}
],
"hasNext": true
}
```

You can listen to payloads by using `toFlow()`:

```kotlin
apolloClient.query(query).toFlow().collectIndexed { index, response ->
// This will be called twice

if (index == 0) {
// First time without the fragment
assertNull(response.data?.computer?.computerFields)
} else if (index == 1) {
// Second time with the fragment
assertNotNull(response.data?.computer?.computerFields)
}
}
```

As always, feedback is very welcome. Let us know what you think of the feature by
either [opening an issue on our GitHub repo](https://github.com/apollographql/apollo-android/issues)
, [joining the community](http://community.apollographql.com/new-topic?category=Help&tags=mobile,client)
or [stopping by our channel in the KotlinLang Slack](https://app.slack.com/client/T09229ZC6/C01A6KM1SBZ)(get your
invite [here](https://slack.kotl.in/)).

## ✨️ [new] Data Builders (#4321)

Apollo Kotlin 3.0 introduced [test builders](https://www.apollographql.com/docs/kotlin/testing/test-builders/). While they are working, they have several limitations. The main one was that being [response based](https://www.apollographql.com/docs/kotlin/advanced/response-based-codegen#the-responsebased-codegen), they could generate a lot of code. Also, they required passing custom scalars using their Json encoding, which is cumbersome.

The [data builders](https://www.apollographql.com/docs/kotlin/testing/data-builders/) are a simpler version of the test builders that generate builders based on schema types. This means most of the generated code is shared between all your implementations except for a top level `Data {}` function in each of your operation:

```kotlin
// Replace
val data = GetHeroQuery.Data {
hero = humanHero {
name = "Luke"
}
}

// With
val data = GetHeroQuery.Data {
hero = buildHuman {
name = "Luke"
}
}
```

## ✨️ [new] Kotlin 1.7 (#4314)

Starting with this release, Apollo Kotlin is built with Kotlin 1.7.10. This doesn't impact Android and JVM projects (the minimum supported version of Kotlin continues to be 1.5) but if you are on a project using Native, you will need to update the Kotlin version to 1.7.0+.

## 👷‍ All changes

* Data builders (#4359, #4338, #4331, #4330, #4328, #4323, #4321)
* Add a flag to disable fieldsCanMerge validation on disjoint types (#4342)
* Re-introduce @defer and use new payload format (#4351)
* Multiplatform: add enableCompatibilityMetadataVariant flag (#4329)
* Remove an unnecessary `file.source().buffer()` (#4326)
* Always use String for defaultValue in introspection Json (#4315)
* Update Kotlin dependency to 1.7.10 (#4314)
* Remove schema and AST from the IR (#4303)

# Version 3.5.0

_2022-08-01_
Expand Down
5 changes: 3 additions & 2 deletions docs/source/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"GraphQL variables": "/advanced/operation-variables",
"Error handling": "/essentials/errors",
"Custom scalars": "/essentials/custom-scalars",
"Fragments": "/essentials/fragments"
"Fragments": "/essentials/fragments",
"@defer support": "/fetching/defer"
},
"Caching": {
"Introduction": "/caching/introduction",
Expand All @@ -64,7 +65,7 @@
"Overview": "/testing/overview",
"Mocking HTTP responses": "/testing/mocking-http-responses",
"Mocking GraphQL responses": "/testing/mocking-graphql-responses",
"Test builders": "/testing/test-builders",
"Data builders": "/testing/data-builders",
"UI Tests": "/testing/ui-tests"
},
"Advanced": {
Expand Down
55 changes: 55 additions & 0 deletions docs/source/fetching/defer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: @defer support
---

> ⚠️ **Defer is a [Stage 1 GraphQL specification proposal](https://github.com/graphql/graphql-spec/blob/main/CONTRIBUTING.md#stage-1-proposal).** Before it reaches acceptance, it might still change in backward incompatible ways. If you have feedback on it, please let us know via [GitHub issues](https://github.com/apollographql/apollo-android/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Defer%20Support]) or in the [Kotlin Slack community](https://slack.kotl.in/).
Apollo Kotlin supports [the `@defer` directive](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md), which allows receiving the fields of certain fragments of a query asynchronously.

For instance consider a service where retrieving basic information from a user is fast, but retrieving the user's friends may take more time. In that case it would make sense to display the basic information in the UI as soon as they arrive, and display a loading indicator in the UI while waiting for the friends.

Applying the `@defer` directive to the fragment selecting the friends fields indicates that we are ready to receive them later, asynchronously:

```graphql
query PersonQuery($personId: ID!) {
person(id: $personId) {
# Basic fields (fast)
id
firstName
lastName

# Friends (slow)
... on User @defer {
friends {
id
}
}
}
}
```

In the generated code for this query, the `onUser` field for the fragment will be nullable. That is because when the initial payload is received from the server, the fields of the fragment are not yet present. A `Person` will be emitted with only the basic fields filled in.

When the fields of the fragment are available, a new `Person` will be emitted, this time with the `onUser` field present and filled with the fields of the fragment.

```kotlin
apolloClient.query(PersonQuery(personId)).toFlow().collect {
println("Received: $it")
if (it.dataAssertNoErrors.person.onUser == null) {
// Initial payload: basic info only
// ...
} else {
// Subsequent payload: with friends
// ...
}
}
```

Will print something like this:

```
Received: Person(id=1, firstName=John, lastName=Doe, onUser=null))
Received: Person(id=1, firstName=John, lastName=Doe, onUser=OnUser(friends=[Friend(id=2), Friend(id=3)]))
```

Note: `@defer` can be used only with the [the `operationBased` codegen](../advanced/response-based-codegen/).
139 changes: 139 additions & 0 deletions docs/source/testing/data-builders.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: Data builders (experimental)
---

> ⚠️ **Data builders are experimental and subject to change.** If you have feedback on them, please let us know via [GitHub issues](https://github.com/apollographql/apollo-android/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Test%20Builders]) or in the [Kotlin Slack community](https://slack.kotl.in/).
Apollo Kotlin generates models for your operations and parsers that create instances of these models from your network responses. Sometimes though, during tests or in other occasions, it is useful to instantiate models manually with known values. Doing so is not as straightforward as it may seem, especially when fragments are used. Creating `operationBased` models requires instantiating every fragment as well as choosing an appropriate `__typename` for each composite type. Data builders make this easier by providing builders that match the structure of the Json document.

__Note: previous versions of Apollo Kotlin used data builders. Data builders are a simpler version of test builders that also plays nicer with custom scalars. If you're looking for the test builders documentation, it's still available [here](./test-builders).__

## Enabling data builders

To enable data, set the `generateDataBuilders` option to `true`:

```kotlin title="build.gradle[.kts]"
apollo {
// ...

// Enable data builder generation
generateDataBuilders.set(true)
}
```

This generates a builder for each composite type in your schema as well as a helper `Data {}` function for each of your operations.

## Example usage

Let's say we're building a test that uses a mocked result of the following query:

```graphql
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
firstName
lastName
age

ship {
model
speed
}

friends {
firstName
lastName
}

... on Droid {
primaryFunction
}

... on Human {
height
}
}
}
```

Here's how we can use the corresponding data builder for that mocked result:

```kotlin
@Test
fun test() {
val data = HeroForEpisodeQuery.Data {
// Set values for particular fields of the query
hero = buildHuman {
firstName = "John"
age = 42
friends = listOf(
buildHuman {
firstName = "Jane"
},
buildHuman {
lastName = "Doe"
}
)
ship = buildStarship {
model = "X-Wing"
}
}
}

assertEquals("John", data.hero.firstName)
assertEquals(42, data.hero.age)
}
```

In this example, the `hero` field is a `Human` object with specified values for `firstName` and `age`. The values for `lastName`and `height` are automatically populated with mock values.
Similarly, values for the ship's speed, the 1st friend's last name and 2nd friend's first name are automatically populated.

> You can replace `buildHuman` above with `buildDroid` to create a `Droid` object instead.
## Configuring default field values

To assign default values to fields, data builders use an implementation of the `FakeResolver` interface. By default, they use an instance of `DefaultFakeResolver`.

The `DefaultFakeResolver` gives each `String` field the field's name as its default value, and it increments a counter as it assigns default values for `Int` fields. It defines similar default behavior for other types.

You can create your _own_ `FakeResolver` implementation (optionally delegating to `DefaultTestResolver` for a head start). You then pass this implementation as a parameter to the `Data` function, like so:

```kotlin {6}
// A TestResolver implementation that assigns -1 to all Int fields
class 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 Ints
else -> delegate.resolveLeaf(context)
}
}

override fun resolveListSize(context: FakeResolverContext): Int {
// Delegate to the default behaviour
return delegate.resolveListSize(context)
}

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

override fun resolveTypename(context: FakeResolverContext): String {
// Delegate to the default behaviour
return delegate.resolveTypename(context)
}
}

@Test
fun test() {
val data = HeroForEpisodeQuery.Data(resolver = myTestResolver) {
hero = buildHuman {
firstName = "John"
}
}

// Unspecified Int field is -1
assertEquals(-1, data.hero.age)
}
```
2 changes: 1 addition & 1 deletion docs/source/testing/test-builders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Test builders (experimental)
---

> ⚠️ **Test builders are experimental and subject to change.** If you have feedback on them, please let us know via [GitHub issues](https://github.com/apollographql/apollo-android/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Test%20Builders]) or in the [Kotlin Slack community](https://slack.kotl.in/).
> ⚠️ Test builders are [response based](https://www.apollographql.com/docs/kotlin/advanced/response-based-codegen#the-responsebased-codegen) and may generate a lot of code. Moving forward, we recommend to use [data builders](./data-builders) instead, which are simpler to use. This page is kept as reference only.
Apollo Kotlin provides **test builders** that enable you to instantiate your GraphQL model classes with default values. Test builders are especially helpful for testing models with a large number of fields or a deeply nested hierarchy. They automatically populate the `__typename` field and deduplicate merged fields whenever possible.

Expand Down
16 changes: 15 additions & 1 deletion tests/defer/src/commonTest/kotlin/test/DeferTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import defer.WithInlineFragmentsQuery
import defer.fragment.ComputerFields
import defer.fragment.ScreenFields
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class DeferTest {
Expand Down Expand Up @@ -243,7 +246,18 @@ class DeferTest {
)

mockServer.enqueueMultipart(jsonList)
val actualResponseList = apolloClient.query(query).toFlow().toList()

apolloClient.query(query).toFlow().collectIndexed { index, response ->
// This will be called twice

if (index == 0) {
// First time without the fragment
assertNull(response.data?.computer?.computerFields)
} else if (index == 0) {
// Second time will have the fragment
assertNotNull(response.data?.computer?.computerFields)
}
}
assertResponseListEquals(expectedDataList, actualResponseList)
}

Expand Down

0 comments on commit 9661fe0

Please sign in to comment.