Skip to content

Commit

Permalink
Merge pull request #4986 from apollographql/release-3.8.2
Browse files Browse the repository at this point in the history
Release 3.8.2
  • Loading branch information
martinbonnin authored May 25, 2023
2 parents 78aaa0d + b828a04 commit b6aad4d
Show file tree
Hide file tree
Showing 45 changed files with 970 additions and 104 deletions.
3 changes: 2 additions & 1 deletion docs/source/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
/tutorial /docs/kotlin/tutorial/00-introduction
/advanced/ui-tests/ /docs/kotlin/testing/ui-tests/
/advanced/test-builders/ /docs/kotlin/testing/test-builders/
/advanced/response-based-codegen/ /docs/kotlin/advanced/codegen-methods/
/advanced/response-based-codegen/ /docs/kotlin/advanced/response-based/
/advanced/codegen-methods/ /docs/kotlin/advanced/response-based/
5 changes: 4 additions & 1 deletion docs/source/advanced/operation-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ query GetTodos($first: Int, $offset: Int) {
Apollo Kotlin generates the following Kotlin code for this query:

```kotlin
class GetTodosQuery(val first: Optional<Int?>, val offset: Optional<Int?>)
class GetTodosQuery(
val first: Optional<Int?> = Optional.Absent,
val offset: Optional<Int?> = Optional.Absent
)
```

You can then selectively provide or omit variable values like so:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
---
title: Code generation methods in Apollo Kotlin
title: Response based codegen
---

Apollo Kotlin provides multiple code generation engines (**codegens**) that you can choose from depending on your use case:
Apollo Kotlin takes your GraphQL operations, generates Kotlin models for them and instantiates them from your JSON responses allowing you to access your data in a type safe way.

There are effectively 3 different domains at play:

| Codegen | Description |
|---------|-------------|
| `operationBased` (default) | Generates models according to the shape of each defined operation. |
| `responseBased` | <p>Generates models according to the shape of each operation's response.</p><p>Compared to `operationBased`, models generated by the `responseBased` codegen are more performant and expose more type information, at the cost of generating more total code. [See details.](#the-responsebased-codegen) </p> |
| `compat` (deprecated) | Similar to `operationBased`, except it generates models that are compatible with Apollo Android v2. This codegen helps projects upgrade from v2 to v3. |
* The GraphQL domain: operations
* The Kotlin domain: models
* The JSON domain: responses

> 💡 For more in-depth information on each codegen, see [this design document](https://github.com/apollographql/apollo-android/blob/main/design-docs/Codegen.md).
By default, Apollo Kotlin generates models that match 1:1 with your GraphQL operations. Inline and named fragments generate synthetic fields, so you can access GraphQL fragments with Kotlin code like `data.hero.onDroid.primaryFunction`. Fragments are classes that can be reused from different operations. This code generation engine (**codegen**) is named `operationBased` because it matches the GraphQL operation.

The Json response may have a different shape than your GraphQL operation though. This is the case when using merged fields or fragments. If you want to access your Kotlin properties as they are in the JSON response, Apollo Kotlin provides a `responseBased` codegen that match 1:1 with the JSON response. GraphQL fragments are represented as Kotlin interfaces, so you can access their fields with Kotlin code like `(data.hero as Droid).primaryFunction`. Because they map to the JSON responses, the `responseBased` models have the property of allowing JSON streaming and/or mapping to dynamic JS objects. But because GraphQL is a very expressive language, [it's also easy to create a GraphQL query that generate a very large JSON response](https://github.com/apollographql/apollo-kotlin/issues/3144).

**For this reason and other [limitations](#limitations-of-responsebased-codegen), we recommend using `operationBased` codegen by default**.

This page first recaps how [`operationBased` codegen](#the-operationbased-codegen-default) works before explaining [`responseBased` codegen](#the-responsebased-codegen). Finally, it lists the different [limitations](#limitations-of-responsebased-codegen) coming with `responseBased` codegen so you can make an informed decision should you use this codegen.

To use a particular codegen, configure `codegenModels` in your Gradle scripts:

Expand Down Expand Up @@ -231,3 +235,11 @@ As a convenience, the `responseBased` codegen generates methods with the name pa
val primaryFunction = hero1.droidFields().primaryFunction
val height = hero2.humanFields().height
```

## Limitations of `responseBased` codegen

1. Because GraphQL is a very expressive language, [it's easy to create a GraphQL query that generate a very large JSON response](https://github.com/apollographql/apollo-kotlin/issues/3144). If you're using a lot of nested fragments, the generated code size can grow exponentially with the nesting level. We have seen relatively small GraphQL queries breaking the JVM limits like [maximum method size](https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-4.html#jvms-4.7.3).
2. When using fragments, data classes must be generated for each operation where the fragments are used. To avoid name clashes, the models are nested and this comes with two side effects:
* The resulting `.class` file name can be very long, breaking the [256 default maximum file name on MacOS](https://apple.stackexchange.com/questions/86611/does-os-x-enforce-a-maximum-filename-length-or-character-restriction).
* Similarly named interfaces might be nested (for fragments). While this is valid in Kotlin, [Java does not allow this](https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.1), and [it will break kapt if you're using it](https://youtrack.jetbrains.com/issue/KT-24272/kapt-generated-code-fails-to-compile-with-symbol-not-found-for-same-named-nested-class).
3. `@include`, `@skip` and `@defer` directives are not supported on fragments in `responseBased` codegen. Supporting them would require generating twice the models each time one of these directive would be used.
4 changes: 2 additions & 2 deletions docs/source/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"Configuration": {
"File types": "/essentials/file-types",
"Client Awareness": "/advanced/client-awareness",
"Codegen methods": "/advanced/codegen-methods",
"Multi Modules": "/advanced/multi-modules",
"Connecting source sets": "/advanced/source-sets",
"Gradle plugin configuration": "/advanced/plugin-configuration"
Expand Down Expand Up @@ -75,7 +74,8 @@
"Using with Java": "/advanced/java",
"RxJava support": "/advanced/rxjava",
"Kotlin native": "/advanced/kotlin-native",
"Apollo AST": "/advanced/apollo-ast"
"Apollo AST": "/advanced/apollo-ast",
"Response based codegen": "/advanced/response-based"
}
}
}
2 changes: 2 additions & 0 deletions docs/source/essentials/custom-scalars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ In addition, the `com.apollographql.apollo3:apollo-adapters` artifact provides t
| `com.apollographql.apollo3.adapter.JavaLocalDateAdapter` | For `java.time.LocalDate` ISO8601 dates |
| `com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter` | For `kotlinx.datetime.LocalDateTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter` | For `java.time.LocalDateTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter` | For `kotlinx.datetime.LocalTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.JavaLocalTimeAdapter` | For `java.time.LocalTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter` | For `java.time.OffsetDateTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.DateAdapter` | For `java.util.Date` ISO8601 dates |
| `com.apollographql.apollo3.adapter.BigDecimalAdapter` | For a Multiplatform `com.apollographql.apollo3.adapter.BigDecimal` class holding big decimal values |
Expand Down
2 changes: 2 additions & 0 deletions libraries/apollo-adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ A few convenient adapters:
| `com.apollographql.apollo3.adapter.JavaLocalDateAdapter` | For `java.time.LocalDate` ISO8601 dates |
| `com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter` | For `kotlinx.datetime.LocalDateTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter` | For `java.time.LocalDateTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter` | For `kotlinx.datetime.LocalTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.JavaLocalTimeAdapter` | For `java.time.LocalTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter` | For `java.time.OffsetDateTime` ISO8601 dates |
| `com.apollographql.apollo3.adapter.DateAdapter` | For `java.util.Date` ISO8601 dates |
| `com.apollographql.apollo3.adapter.BigDecimalAdapter` | For a Multiplatform `com.apollographql.apollo3.adapter.BigDecimal` class holding big decimal values |
16 changes: 16 additions & 0 deletions libraries/apollo-adapters/api/apollo-adapters.api
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ public final class com/apollographql/apollo3/adapter/JavaLocalDateTimeAdapter :
public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/time/LocalDateTime;)V
}

public final class com/apollographql/apollo3/adapter/JavaLocalTimeAdapter : com/apollographql/apollo3/api/Adapter {
public static final field INSTANCE Lcom/apollographql/apollo3/adapter/JavaLocalTimeAdapter;
public synthetic fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/lang/Object;
public fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/time/LocalTime;
public synthetic fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/lang/Object;)V
public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/time/LocalTime;)V
}

public final class com/apollographql/apollo3/adapter/JavaOffsetDateTimeAdapter : com/apollographql/apollo3/api/Adapter {
public static final field INSTANCE Lcom/apollographql/apollo3/adapter/JavaOffsetDateTimeAdapter;
public synthetic fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/lang/Object;
Expand Down Expand Up @@ -74,3 +82,11 @@ public final class com/apollographql/apollo3/adapter/KotlinxLocalDateTimeAdapter
public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Lkotlinx/datetime/LocalDateTime;)V
}

public final class com/apollographql/apollo3/adapter/KotlinxLocalTimeAdapter : com/apollographql/apollo3/api/Adapter {
public static final field INSTANCE Lcom/apollographql/apollo3/adapter/KotlinxLocalTimeAdapter;
public synthetic fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/lang/Object;
public fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Lkotlinx/datetime/LocalTime;
public synthetic fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/lang/Object;)V
public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Lkotlinx/datetime/LocalTime;)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.apollographql.apollo3.api.json.JsonWriter
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime

/**
* An [Adapter] that converts an ISO 8601 String like "2010-06-01T22:19:44.475Z" to/from
Expand Down Expand Up @@ -55,3 +56,19 @@ object KotlinxLocalDateAdapter : Adapter<LocalDate> {
writer.value(value.toString())
}
}

/**
* An [Adapter] that converts an ISO 8601 String like "14:35:00" to/from
* a [kotlinx.datetime.LocalDate]
*
* It requires Android Gradle plugin 4.0 or newer and [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).
*/
object KotlinxLocalTimeAdapter : Adapter<LocalTime> {
override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): LocalTime {
return LocalTime.parse(reader.nextString()!!)
}

override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: LocalTime) {
writer.value(value.toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.apollographql.apollo3.api.json.JsonWriter
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter

Expand Down Expand Up @@ -85,3 +86,21 @@ object JavaOffsetDateTimeAdapter : Adapter<OffsetDateTime> {
writer.value(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
}

/**
* An [Adapter] that converts a time to/from [java.time.LocalTime]
*
* Examples:
* - "14:35:00"
*
* It requires Android Gradle plugin 4.0 or newer and [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).
*/
object JavaLocalTimeAdapter : Adapter<LocalTime> {
override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): LocalTime {
return LocalTime.parse(reader.nextString())
}

override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: LocalTime) {
writer.value(value.toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import com.apollographql.apollo3.adapter.DateAdapter
import com.apollographql.apollo3.adapter.JavaInstantAdapter
import com.apollographql.apollo3.adapter.JavaLocalDateAdapter
import com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter
import com.apollographql.apollo3.adapter.JavaLocalTimeAdapter
import com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter
import com.apollographql.apollo3.adapter.KotlinxInstantAdapter
import com.apollographql.apollo3.adapter.KotlinxLocalDateAdapter
import com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter
import com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter
import com.apollographql.apollo3.api.Adapter
import com.apollographql.apollo3.api.CustomScalarAdapters
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader
Expand Down Expand Up @@ -83,6 +85,15 @@ class JavaTimeAdaptersTest {
assertEquals("2010-06-01", JavaLocalDateAdapter.toJson(localDate))
}

@Test
fun localTime() {
val localTime = JavaLocalTimeAdapter.fromJson("14:35:20")
assertEquals(14, localTime.hour)
assertEquals(35, localTime.minute)
assertEquals(20, localTime.second)
assertEquals("14:35:20", JavaLocalTimeAdapter.toJson(localTime))
}

@Test
fun kotlinxInstant() {
var instant = KotlinxInstantAdapter.fromJson("2010-06-01T22:19:44.475Z")
Expand All @@ -109,4 +120,13 @@ class JavaTimeAdaptersTest {
assertEquals("2010-06-01", KotlinxLocalDateAdapter.toJson(localDate))
}

@Test
fun kotlinxLocalTime() {
val localTime = KotlinxLocalTimeAdapter.fromJson("14:35:20")
assertEquals(14, localTime.hour)
assertEquals(35, localTime.minute)
assertEquals(20, localTime.second)
assertEquals("14:35:20", KotlinxLocalTimeAdapter.toJson(localTime))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ internal fun ValidationScope.validateAndCoerceValue(
is GQLNonNullType -> {
return validateAndCoerceValue(value, expectedType.type, hasLocationDefaultValue, registerVariableUsage)
}

is GQLListType -> {
val coercedValue = if (value !is GQLListValue) {
/**
Expand All @@ -90,21 +91,25 @@ internal fun ValidationScope.validateAndCoerceValue(
}
)
}

is GQLNamedType -> {
when (val expectedTypeDefinition = typeDefinitions[expectedType.name]) {
is GQLInputObjectTypeDefinition -> {
return validateAndCoerceInputObject(value, expectedTypeDefinition, registerVariableUsage)
}

is GQLScalarTypeDefinition -> {
if (!expectedTypeDefinition.isBuiltIn()) {
// custom scalar types are passed through
return value
}
return validateAndCoerceScalar(value, expectedType)
}

is GQLEnumTypeDefinition -> {
return validateAndCoerceEnum(value, expectedTypeDefinition)
}

else -> {
registerIssue("Value cannot be of non-input type ${expectedType.pretty()}", value.sourceLocation)
return value
Expand Down Expand Up @@ -137,15 +142,17 @@ private fun ValidationScope.validateAndCoerceInputObject(
) {
registerIssue(message = "No value passed for required inputField `${inputValueDefinition.name}`", sourceLocation = value.sourceLocation)
}
if (inputValueDefinition.directives.findDeprecationReason() != null) {
}
value.fields.forEach { field ->
val inputValueDefinition = expectedTypeDefinition.inputFields.firstOrNull { it.name == field.name }
if (inputValueDefinition?.directives?.findDeprecationReason() != null) {
issues.add(
Issue.DeprecatedUsage(
message = "Use of deprecated input field `${inputValueDefinition.name}`",
sourceLocation = value.sourceLocation
)
)
}

}

return GQLObjectValue(fields = value.fields.mapNotNull { field ->
Expand Down Expand Up @@ -192,6 +199,7 @@ private fun ValidationScope.validateAndCoerceScalar(value: GQLValue, expectedTyp
}
value
}

"Float" -> {
when (value) {
is GQLFloatValue -> value
Expand All @@ -203,25 +211,29 @@ private fun ValidationScope.validateAndCoerceScalar(value: GQLValue, expectedTyp
}
}
}

"String" -> {
if (value !is GQLStringValue) {
registerIssue(value, expectedType)
}
value
}

"Boolean" -> {
if (value !is GQLBooleanValue) {
registerIssue(value, expectedType)
}
value
}

"ID" -> {
// 3.5.5 ID can be either string or int
if (value !is GQLStringValue && value !is GQLIntValue) {
registerIssue(value, expectedType)
}
value
}

else -> {
registerIssue(value, expectedType)
value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private fun ValidationScope.mergeEnumValues(
val existing = result.firstOrNull { it.name == other.name }
result += if (existing != null) {
result.remove(existing)
other.copy(directives = mergeDirectives(existing.directives, other.directives))
existing.copy(directives = mergeDirectives(existing.directives, other.directives))
} else {
other
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.apollographql.apollo3.graphql.ast.test.validation

import com.apollographql.apollo3.ast.SourceLocation
import com.apollographql.apollo3.ast.introspection.toSchema
import com.apollographql.apollo3.ast.parseAsGQLDocument
import com.apollographql.apollo3.ast.validateAsExecutable
import okio.buffer
import okio.source
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals

class OperationValidationTest {
@Test
fun testAddRequiredFields() {
val schema = File("src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphqls").toSchema()
val operations = File("src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphql")
.source()
.buffer()
.parseAsGQLDocument()
.valueAssertNoErrors()
val operationIssues = operations.validateAsExecutable(schema).issues
assertEquals(1, operationIssues.size)
assertEquals("Use of deprecated input field `deprecatedParameter`", operationIssues[0].message)
assertEquals(SourceLocation(12, 17, null).pretty(), operationIssues[0].sourceLocation.pretty())
}
}
Loading

0 comments on commit b6aad4d

Please sign in to comment.