From 6ec9ab7a9c4fbc18792a96620bea2c04a53e5dd6 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 23 Aug 2024 12:09:06 +0200 Subject: [PATCH 1/7] Add tests for declarative cache control --- gradle/libs.versions.toml | 2 +- .../cache/normalized/api/MaxAgeProvider.kt | 18 ++- settings.gradle.kts | 1 + tests/expiration/build.gradle.kts | 10 +- .../{ => declarative}/operations.graphql | 0 .../graphql/declarative/schema.graphqls | 68 +++++++++++ .../graphql/programmatic/operations.graphql | 112 ++++++++++++++++++ .../{ => programmatic}/schema.graphqls | 0 .../ClientAndServerSideExpirationTest.kt | 6 +- .../kotlin/ClientSideExpirationTest.kt | 99 ++++++++++++++-- .../SchemaCoordinatesMaxAgeProviderTest.kt | 55 +++++++-- .../kotlin/ServerSideExpirationTest.kt | 2 +- tests/settings.gradle.kts | 1 + 13 files changed, 339 insertions(+), 35 deletions(-) rename tests/expiration/src/commonMain/graphql/{ => declarative}/operations.graphql (100%) create mode 100644 tests/expiration/src/commonMain/graphql/declarative/schema.graphqls create mode 100644 tests/expiration/src/commonMain/graphql/programmatic/operations.graphql rename tests/expiration/src/commonMain/graphql/{ => programmatic}/schema.graphqls (100%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2d6f609..eb93dfef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin-plugin = "2.0.0" android-plugin = "8.2.2" -apollo = "4.0.0" +apollo = "4.0.1-SNAPSHOT" okio = "3.9.0" atomicfu = "0.23.1" # Must be the same version as the one used by apollo-testing-support or native compilation will fail sqldelight = "2.0.1" diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt index 0d915833..bc3620f5 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt @@ -3,6 +3,7 @@ package com.apollographql.cache.normalized.api import com.apollographql.apollo.api.CompiledField import com.apollographql.apollo.api.isComposite import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds interface MaxAgeProvider { /** @@ -46,7 +47,7 @@ sealed interface MaxAge { * Then the lowest of the field's max age and its parent field's max age is returned. */ class SchemaCoordinatesMaxAgeProvider( - private val coordinatesToMaxAges: Map, + private val maxAges: Map, private val defaultMaxAge: Duration, ) : MaxAgeProvider { override fun getMaxAge(maxAgeContext: MaxAgeContext): Duration { @@ -58,7 +59,7 @@ class SchemaCoordinatesMaxAgeProvider( val fieldName = maxAgeContext.fieldPath.last().name val fieldParentTypeName = maxAgeContext.fieldPath[maxAgeContext.fieldPath.lastIndex - 1].type.rawType().name val fieldCoordinates = "$fieldParentTypeName.$fieldName" - val computedFieldMaxAge = when (val fieldMaxAge = coordinatesToMaxAges[fieldCoordinates]) { + val computedFieldMaxAge = when (val fieldMaxAge = maxAges[fieldCoordinates]) { is MaxAge.Duration -> { fieldMaxAge.duration } @@ -84,7 +85,7 @@ class SchemaCoordinatesMaxAgeProvider( private fun getTypeMaxAge(maxAgeContext: MaxAgeContext): Duration { val field = maxAgeContext.fieldPath.last() val fieldTypeName = field.type.rawType().name - return when (val typeMaxAge = coordinatesToMaxAges[fieldTypeName]) { + return when (val typeMaxAge = maxAges[fieldTypeName]) { is MaxAge.Duration -> { typeMaxAge.duration } @@ -113,3 +114,14 @@ class SchemaCoordinatesMaxAgeProvider( } } } + +fun SchemaCoordinatesMaxAgeProvider(maxAges: Map, defaultMaxAge: Duration): MaxAgeProvider { + val mappedMaxAges = maxAges.mapValues { + if (it.value == -1) { + MaxAge.Inherit + } else { + MaxAge.Duration(it.value.seconds) + } + } + return SchemaCoordinatesMaxAgeProvider(mappedMaxAges, defaultMaxAge) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d13d8bb5..51113f35 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ pluginManagement { listOf(repositories, dependencyResolutionManagement.repositories).forEach { + it.mavenLocal() it.mavenCentral() it.google() } diff --git a/tests/expiration/build.gradle.kts b/tests/expiration/build.gradle.kts index f662eb8e..7bf8d82e 100644 --- a/tests/expiration/build.gradle.kts +++ b/tests/expiration/build.gradle.kts @@ -63,7 +63,13 @@ kotlin { } apollo { - service("service") { - packageName.set("sqlite") + service("programmatic") { + packageName.set("programmatic") + srcDir("src/commonMain/graphql/programmatic") + } + + service("declarative") { + packageName.set("declarative") + srcDir("src/commonMain/graphql/declarative") } } diff --git a/tests/expiration/src/commonMain/graphql/operations.graphql b/tests/expiration/src/commonMain/graphql/declarative/operations.graphql similarity index 100% rename from tests/expiration/src/commonMain/graphql/operations.graphql rename to tests/expiration/src/commonMain/graphql/declarative/operations.graphql diff --git a/tests/expiration/src/commonMain/graphql/declarative/schema.graphqls b/tests/expiration/src/commonMain/graphql/declarative/schema.graphqls new file mode 100644 index 00000000..264ec286 --- /dev/null +++ b/tests/expiration/src/commonMain/graphql/declarative/schema.graphqls @@ -0,0 +1,68 @@ +extend schema @link( + url: "https://specs.apollo.dev/cache/v0.1", + import: ["@cacheControl", "@cacheControlField"] +) + +type Query { + user: User + company: Company + products: [Product] + product(id: ID!): Product + node(id: ID!): Node + book: Book + cachedBook: Book @cacheControl(maxAge: 60) + reader: Reader @cacheControl(maxAge: 40) + currentUserId: String +} + +type User @cacheControl(maxAge: 10) { + name: String! @cacheControl(maxAge: 5) + email: String! @cacheControl(maxAge: 2) + admin: Boolean +} + +type Company { + id: ID! +} + +interface Node @cacheControl(maxAge: 30) { + id: ID! +} + +type Product implements Node @cacheControl(maxAge: 60) { + id: ID! + name: String! + price: Float! + colors: [ProductColor] +} + +union ProductColor = StandardColor | CustomColor + +type StandardColor { + color: Color +} + +enum Color { + BLACK + WHITE + RED + GREEN + BLUE + ORANGE +} + +type CustomColor { + red: Int! + green: Int! + blue: Int! +} + + +type Book { + title: String + cachedTitle: String @cacheControl(maxAge: 30) +} + +type Reader { + book: Book @cacheControl(inheritMaxAge: true) +} diff --git a/tests/expiration/src/commonMain/graphql/programmatic/operations.graphql b/tests/expiration/src/commonMain/graphql/programmatic/operations.graphql new file mode 100644 index 00000000..0de01a4f --- /dev/null +++ b/tests/expiration/src/commonMain/graphql/programmatic/operations.graphql @@ -0,0 +1,112 @@ +query GetUser { + user { + name + email + admin + } +} + +query GetUserAdmin { + user { + admin + } +} + +query GetUserEmail { + user { + email + } +} + +query GetUserName { + user { + name + } +} + +query GetCompany { + company { + id + } +} + +# maxAge: 0 +# Query.book doesn't set a maxAge and it's a root field (default 0). +query GetBookTitle { + book { # 0 + cachedTitle # 30 + } +} + +# maxAge: 60 +# Query.cachedBook has a maxAge of 60, and Book.title is a scalar, so it +# inherits maxAge from its parent by default. +query GetCachedBookTitle { + cachedBook { # 60 + title # inherits + } +} + +# maxAge: 30 +# Query.cachedBook has a maxAge of 60, but Book.cachedTitle has +# a maxAge of 30. +query GetCachedBookCachedTitle { + cachedBook { # 60 + cachedTitle # 30 + } +} + +# maxAge: 40 +# Query.reader has a maxAge of 40. Reader.Book is set to +# inheritMaxAge from its parent, and Book.title is a scalar +# that inherits maxAge from its parent by default. +query GetReaderBookTitle { + reader { # 40 + book { # inherits + title # inherits + } + } +} + +query GetProducts { + products { + id + name + price + colors { + ... on StandardColor { + color + } + ... on CustomColor { + red + green + blue + } + } + } + currentUserId +} + +query GetProduct { + product(id: "1") { + id + name + price + colors { + ... on StandardColor { + color + } + ... on CustomColor { + red + green + blue + } + } + } +} + +query GetNodes { + node(id: "1") { + id + } +} diff --git a/tests/expiration/src/commonMain/graphql/schema.graphqls b/tests/expiration/src/commonMain/graphql/programmatic/schema.graphqls similarity index 100% rename from tests/expiration/src/commonMain/graphql/schema.graphqls rename to tests/expiration/src/commonMain/graphql/programmatic/schema.graphqls diff --git a/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt index 1aa3ce4a..d058e793 100644 --- a/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt +++ b/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt @@ -18,9 +18,9 @@ import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory import com.apollographql.cache.normalized.storeExpirationDate import com.apollographql.mockserver.MockResponse import com.apollographql.mockserver.MockServer -import sqlite.GetUserEmailQuery -import sqlite.GetUserNameQuery -import sqlite.GetUserQuery +import programmatic.GetUserEmailQuery +import programmatic.GetUserNameQuery +import programmatic.GetUserQuery import kotlin.test.Test import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds diff --git a/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt index e797d566..dc7d605e 100644 --- a/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt +++ b/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt @@ -22,11 +22,11 @@ import com.apollographql.cache.normalized.fetchPolicy import com.apollographql.cache.normalized.maxStale import com.apollographql.cache.normalized.normalizedCache import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory -import sqlite.GetCompanyQuery -import sqlite.GetUserAdminQuery -import sqlite.GetUserEmailQuery -import sqlite.GetUserNameQuery -import sqlite.GetUserQuery +import programmatic.GetCompanyQuery +import programmatic.GetUserAdminQuery +import programmatic.GetUserEmailQuery +import programmatic.GetUserNameQuery +import programmatic.GetUserQuery import kotlin.test.Test import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds @@ -48,18 +48,33 @@ class ClientSideExpirationTest { } @Test - fun schemaCoordinatesMaxAgeMemoryCache() { - schemaCoordinatesMaxAge(MemoryCacheFactory()) + fun programmaticMaxAgeMemoryCache() { + programmaticMaxAge(MemoryCacheFactory()) } @Test - fun schemaCoordinatesMaxAgeSqlCache() { - schemaCoordinatesMaxAge(SqlNormalizedCacheFactory()) + fun programmaticMaxAgeSqlCache() { + programmaticMaxAge(SqlNormalizedCacheFactory()) } @Test - fun schemaCoordinatesMaxAgeChainedCache() { - schemaCoordinatesMaxAge(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) + fun programmaticMaxAgeChainedCache() { + programmaticMaxAge(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) + } + + @Test + fun declarativeMaxAgeMemoryCache() { + declarativeMaxAge(MemoryCacheFactory()) + } + + @Test + fun declarativeMaxAgeSqlCache() { + declarativeMaxAge(SqlNormalizedCacheFactory()) + } + + @Test + fun declarativeMaxAgeChainedCache() { + declarativeMaxAge(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) } @@ -102,7 +117,7 @@ class ClientSideExpirationTest { assertTrue(response2.data?.user?.name == "John") } - private fun schemaCoordinatesMaxAge(normalizedCacheFactory: NormalizedCacheFactory) = runTest { + private fun programmaticMaxAge(normalizedCacheFactory: NormalizedCacheFactory) = runTest { val maxAgeProvider = SchemaCoordinatesMaxAgeProvider( mapOf( "User" to MaxAge.Duration(10.seconds), @@ -174,6 +189,66 @@ class ClientSideExpirationTest { } } + private fun declarativeMaxAge(normalizedCacheFactory: NormalizedCacheFactory) = runTest { + val maxAgeProvider = SchemaCoordinatesMaxAgeProvider( + declarative.cache.Cache.maxAges, + defaultMaxAge = 20.seconds, + ) + + val client = ApolloClient.Builder() + .normalizedCache( + normalizedCacheFactory = normalizedCacheFactory, + cacheResolver = ExpirationCacheResolver(maxAgeProvider), + ) + .serverUrl("unused") + .build() + client.apolloStore.clearAll() + + // Store records 25 seconds ago, more than default max age: should cache miss + mergeCompanyQueryResults(client, 25) + var e = client.query(declarative.GetCompanyQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 15 seconds ago, less than default max age: should not cache miss + mergeCompanyQueryResults(client, 15) + // Company fields are not configured so the default max age should be used + val companyResponse = client.query(declarative.GetCompanyQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(companyResponse.data?.company?.id == "42") + + + // Store records 15 seconds ago, more than max age for User: should cache miss + mergeUserQueryResults(client, 15) + e = client.query(declarative.GetUserAdminQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 5 seconds ago, less than max age for User: should not cache miss + mergeUserQueryResults(client, 5) + val userAdminResponse = client.query(declarative.GetUserAdminQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userAdminResponse.data?.user?.admin == true) + + + // Store records 10 seconds ago, more than max age for User.name: should cache miss + mergeUserQueryResults(client, 10) + e = client.query(declarative.GetUserNameQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 2 seconds ago, less than max age for User.name: should not cache miss + mergeUserQueryResults(client, 2) + val userNameResponse = client.query(declarative.GetUserNameQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userNameResponse.data?.user?.name == "John") + + + // Store records 5 seconds ago, more than max age for User.email: should cache miss + mergeUserQueryResults(client, 5) + e = client.query(GetUserEmailQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 1 second ago, less than max age for User.email: should not cache miss + mergeUserQueryResults(client, 1) + val userEmailResponse = client.query(GetUserEmailQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userEmailResponse.data?.user?.email == "john@doe.com") + } + private fun mergeUserQueryResults(client: ApolloClient, secondsAgo: Int) { val data = GetUserQuery.Data(GetUserQuery.User("John", "john@doe.com", true)) val records = GetUserQuery().normalize(data, CustomScalarAdapters.Empty, TypePolicyCacheKeyGenerator).values diff --git a/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt b/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt index 9ccff8e3..01a664cc 100644 --- a/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt +++ b/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt @@ -4,23 +4,24 @@ import com.apollographql.apollo.api.CompiledField import com.apollographql.cache.normalized.api.MaxAge import com.apollographql.cache.normalized.api.MaxAgeContext import com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider -import sqlite.GetBookTitleQuery -import sqlite.GetCachedBookCachedTitleQuery -import sqlite.GetCachedBookTitleQuery -import sqlite.GetNodesQuery -import sqlite.GetProductQuery -import sqlite.GetProductsQuery -import sqlite.GetReaderBookTitleQuery +import declarative.cache.Cache +import programmatic.GetBookTitleQuery +import programmatic.GetCachedBookCachedTitleQuery +import programmatic.GetCachedBookTitleQuery +import programmatic.GetNodesQuery +import programmatic.GetProductQuery +import programmatic.GetProductsQuery +import programmatic.GetReaderBookTitleQuery import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds class SchemaCoordinatesMaxAgeProviderTest { @Test - fun apolloServerExample() { + fun programmaticApolloServerExample() { // Taken from https://www.apollographql.com/docs/apollo-server/performance/caching/#example-maxage-calculations val provider = SchemaCoordinatesMaxAgeProvider( - coordinatesToMaxAges = mapOf( + maxAges = mapOf( "Query.cachedBook" to MaxAge.Duration(60.seconds), "Query.reader" to MaxAge.Duration(40.seconds), "Book.cachedTitle" to MaxAge.Duration(30.seconds), @@ -50,10 +51,38 @@ class SchemaCoordinatesMaxAgeProviderTest { assertEquals(40.seconds, maxAge) } + @Test + fun declarativeApolloServerExample() { + val provider = SchemaCoordinatesMaxAgeProvider( + maxAges = Cache.maxAges, + defaultMaxAge = 0.seconds, + ) + + var maxAge = provider.getMaxAge( + MaxAgeContext(declarative.GetBookTitleQuery().rootField().path("book", "cachedTitle")) + ) + assertEquals(0.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(declarative.GetCachedBookTitleQuery().rootField().path("cachedBook", "title")) + ) + assertEquals(60.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(declarative.GetCachedBookCachedTitleQuery().rootField().path("cachedBook", "cachedTitle")) + ) + assertEquals(30.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(declarative.GetReaderBookTitleQuery().rootField().path("reader", "book", "title")) + ) + assertEquals(40.seconds, maxAge) + } + @Test fun interfaceAndObject() { val provider = SchemaCoordinatesMaxAgeProvider( - coordinatesToMaxAges = mapOf( + maxAges = mapOf( "Product" to MaxAge.Duration(60.seconds), "Node" to MaxAge.Duration(30.seconds), ), @@ -74,7 +103,7 @@ class SchemaCoordinatesMaxAgeProviderTest { @Test fun fallbackValue() { val provider1 = SchemaCoordinatesMaxAgeProvider( - coordinatesToMaxAges = mapOf(), + maxAges = mapOf(), defaultMaxAge = 12.seconds, ) var maxAge = provider1.getMaxAge( @@ -96,7 +125,7 @@ class SchemaCoordinatesMaxAgeProviderTest { assertEquals(12.seconds, maxAge) val provider2 = SchemaCoordinatesMaxAgeProvider( - coordinatesToMaxAges = mapOf( + maxAges = mapOf( "Product" to MaxAge.Duration(60.seconds), ), defaultMaxAge = 12.seconds, @@ -118,5 +147,5 @@ class SchemaCoordinatesMaxAgeProviderTest { private fun CompiledField.field(name: String): CompiledField = selections.firstOrNull { (it as CompiledField).name == name } as CompiledField -private fun CompiledField.path(vararg path: String): List = +fun CompiledField.path(vararg path: String): List = path.fold(listOf(this)) { acc, name -> acc + acc.last().field(name) } diff --git a/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt index 9872273d..cd5f1a13 100644 --- a/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt +++ b/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt @@ -18,7 +18,7 @@ import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory import com.apollographql.cache.normalized.storeExpirationDate import com.apollographql.mockserver.MockResponse import com.apollographql.mockserver.MockServer -import sqlite.GetUserQuery +import programmatic.GetUserQuery import kotlin.test.Test import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts index 2cc7cc1a..002a82cd 100644 --- a/tests/settings.gradle.kts +++ b/tests/settings.gradle.kts @@ -1,6 +1,7 @@ pluginManagement { listOf(repositories, dependencyResolutionManagement.repositories).forEach { it.apply { + mavenLocal() mavenCentral() } } From 24019230df77ec45ae21f8a3e4e020e53080cdb3 Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 24 Sep 2024 16:35:33 +0200 Subject: [PATCH 2/7] Don't use Apollo Snapshots --- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 1 - tests/settings.gradle.kts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb93dfef..f2d6f609 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin-plugin = "2.0.0" android-plugin = "8.2.2" -apollo = "4.0.1-SNAPSHOT" +apollo = "4.0.0" okio = "3.9.0" atomicfu = "0.23.1" # Must be the same version as the one used by apollo-testing-support or native compilation will fail sqldelight = "2.0.1" diff --git a/settings.gradle.kts b/settings.gradle.kts index 51113f35..d13d8bb5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,5 @@ pluginManagement { listOf(repositories, dependencyResolutionManagement.repositories).forEach { - it.mavenLocal() it.mavenCentral() it.google() } diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts index 002a82cd..2cc7cc1a 100644 --- a/tests/settings.gradle.kts +++ b/tests/settings.gradle.kts @@ -1,7 +1,6 @@ pluginManagement { listOf(repositories, dependencyResolutionManagement.repositories).forEach { it.apply { - mavenLocal() mavenCentral() } } From b3a1c2378482780f3a1f2588436314a93df88a43 Mon Sep 17 00:00:00 2001 From: BoD Date: Wed, 25 Sep 2024 17:03:08 +0200 Subject: [PATCH 3/7] Use version catalog consistently --- build.gradle.kts | 3 ++- gradle/libs.versions.toml | 3 ++- normalized-cache-incubating/build.gradle.kts | 4 ++-- normalized-cache-sqlite-incubating/build.gradle.kts | 6 +++--- tests/build.gradle.kts | 2 +- tests/expiration/build.gradle.kts | 4 ++-- tests/normalized-cache/build.gradle.kts | 4 ++-- tests/pagination/build.gradle.kts | 4 ++-- tests/settings.gradle.kts | 4 ++-- 9 files changed, 18 insertions(+), 16 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6e1f45fa..8b80d18a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,8 @@ import com.gradleup.librarian.gradle.librarianRoot plugins { - alias(libs.plugins.kotlin).apply(false) + alias(libs.plugins.kotlin.multiplatform).apply(false) + alias(libs.plugins.kotlin.jvm).apply(false) alias(libs.plugins.android).apply(false) alias(libs.plugins.librarian).apply(false) alias(libs.plugins.atomicfu).apply(false) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2d6f609..4d366688 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,8 @@ androidx-sqlite-framework = { group = "androidx.sqlite", name = "sqlite-framewor androidx-startup-runtime = "androidx.startup:startup-runtime:1.1.1" [plugins] -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-plugin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-plugin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-plugin" } android = { id = "com.android.library", version.ref = "android-plugin" } librarian = { id = "com.gradleup.librarian", version.ref = "librarian" } atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin-plugin" } diff --git a/normalized-cache-incubating/build.gradle.kts b/normalized-cache-incubating/build.gradle.kts index 9c88fa9a..8e1e8379 100644 --- a/normalized-cache-incubating/build.gradle.kts +++ b/normalized-cache-incubating/build.gradle.kts @@ -3,8 +3,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { - id("org.jetbrains.kotlin.multiplatform") - id("kotlinx-atomicfu") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.atomicfu) } librarianModule(true) diff --git a/normalized-cache-sqlite-incubating/build.gradle.kts b/normalized-cache-sqlite-incubating/build.gradle.kts index cbc3da6d..9393507b 100644 --- a/normalized-cache-sqlite-incubating/build.gradle.kts +++ b/normalized-cache-sqlite-incubating/build.gradle.kts @@ -2,9 +2,9 @@ import com.gradleup.librarian.gradle.librarianModule import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { - id("org.jetbrains.kotlin.multiplatform") - id("com.android.library") - id("app.cash.sqldelight") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android) + alias(libs.plugins.sqldelight) } librarianModule(true) diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index c30fd794..2983a63a 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -6,6 +6,6 @@ buildscript { } plugins { - alias(libs.plugins.kotlin).apply(false) + alias(libs.plugins.kotlin.multiplatform).apply(false) alias(libs.plugins.apollo).apply(false) } diff --git a/tests/expiration/build.gradle.kts b/tests/expiration/build.gradle.kts index 7bf8d82e..3d44afc9 100644 --- a/tests/expiration/build.gradle.kts +++ b/tests/expiration/build.gradle.kts @@ -1,8 +1,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { - id("org.jetbrains.kotlin.multiplatform") - id("com.apollographql.apollo") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.apollo) } kotlin { diff --git a/tests/normalized-cache/build.gradle.kts b/tests/normalized-cache/build.gradle.kts index f662eb8e..1b620954 100644 --- a/tests/normalized-cache/build.gradle.kts +++ b/tests/normalized-cache/build.gradle.kts @@ -1,8 +1,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { - id("org.jetbrains.kotlin.multiplatform") - id("com.apollographql.apollo") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.apollo) } kotlin { diff --git a/tests/pagination/build.gradle.kts b/tests/pagination/build.gradle.kts index c6deb8cb..9d1a8da1 100644 --- a/tests/pagination/build.gradle.kts +++ b/tests/pagination/build.gradle.kts @@ -2,8 +2,8 @@ import com.apollographql.apollo.annotations.ApolloExperimental import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { - id("org.jetbrains.kotlin.multiplatform") - id("com.apollographql.apollo") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.apollo) } kotlin { diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts index 2cc7cc1a..7b7aa42d 100644 --- a/tests/settings.gradle.kts +++ b/tests/settings.gradle.kts @@ -1,3 +1,5 @@ +includeBuild("../") + pluginManagement { listOf(repositories, dependencyResolutionManagement.repositories).forEach { it.apply { @@ -15,5 +17,3 @@ rootProject.projectDir .forEach { include(it.name) } - -includeBuild("../") From c5ef00ec7d7eeb4dc7c4d26ee45fc10c017ce91c Mon Sep 17 00:00:00 2001 From: BoD Date: Wed, 25 Sep 2024 17:41:22 +0200 Subject: [PATCH 4/7] Add Gradle plugin --- gradle/libs.versions.toml | 5 + normalized-cache-gradle-plugin/README.md | 1 + .../api/normalized-cache-gradle-plugin.api | 30 ++++ .../build.gradle.kts | 27 +++ .../librarian.properties | 1 + .../cache/gradle/ApolloCachePlugin.kt | 22 +++ .../gradle/GenerateApolloCacheSourcesTask.kt | 49 ++++++ .../cache/gradle/internal/codegen/Codegen.kt | 59 +++++++ .../gradle/internal/codegen/SchemaReader.kt | 164 ++++++++++++++++++ settings.gradle.kts | 6 +- tests/build.gradle.kts | 1 + tests/expiration/build.gradle.kts | 11 ++ 12 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 normalized-cache-gradle-plugin/README.md create mode 100644 normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api create mode 100644 normalized-cache-gradle-plugin/build.gradle.kts create mode 100644 normalized-cache-gradle-plugin/librarian.properties create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d366688..25979324 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ apollo-mpp-utils = { group = "com.apollographql.apollo", name = "apollo-mpp-util apollo-testing-support = { group = "com.apollographql.apollo", name = "apollo-testing-support", version.ref = "apollo" } apollo-runtime = { group = "com.apollographql.apollo", name = "apollo-runtime", version.ref = "apollo" } apollo-mockserver = "com.apollographql.mockserver:apollo-mockserver:0.0.1" +apollo-ast = { group = "com.apollographql.apollo", name = "apollo-ast", version.ref = "apollo" } atomicfu-library = { group = "org.jetbrains.kotlinx", name = "atomicfu", version.ref = "atomicfu" } kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test" } # the Kotlin plugin resolves the version kotlin-test-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit" } # the Kotlin plugin resolves the version @@ -28,6 +29,9 @@ slf4j-nop = "org.slf4j:slf4j-nop:2.0.13" androidx-sqlite = { group = "androidx.sqlite", name = "sqlite", version.ref = "androidx-sqlite" } androidx-sqlite-framework = { group = "androidx.sqlite", name = "sqlite-framework", version.ref = "androidx-sqlite" } androidx-startup-runtime = "androidx.startup:startup-runtime:1.1.1" +kgp-min = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version = "1.9.0" } +gradle-api-min = { group = "dev.gradleplugins", name = "gradle-api", version = "8.0" } +kotlin-poet = { group = "com.squareup", name = "kotlinpoet", version = "1.18.1" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-plugin" } @@ -37,3 +41,4 @@ librarian = { id = "com.gradleup.librarian", version.ref = "librarian" } atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin-plugin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } apollo = { id = "com.apollographql.apollo", version.ref = "apollo" } +apollo-cache = { id = "com.apollographql.cache" } diff --git a/normalized-cache-gradle-plugin/README.md b/normalized-cache-gradle-plugin/README.md new file mode 100644 index 00000000..af6d5de2 --- /dev/null +++ b/normalized-cache-gradle-plugin/README.md @@ -0,0 +1 @@ +# Module normalized-cache-gradle-plugin diff --git a/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api b/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api new file mode 100644 index 00000000..c1b9cb62 --- /dev/null +++ b/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api @@ -0,0 +1,30 @@ +public abstract class com/apollographql/cache/gradle/ApolloCacheExtension { + public fun (Lorg/gradle/api/Project;)V + public final fun getProject ()Lorg/gradle/api/Project; + public final fun service (Ljava/lang/String;Lorg/gradle/api/Action;)V +} + +public abstract class com/apollographql/cache/gradle/ApolloCachePlugin : org/gradle/api/Plugin { + public fun ()V + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public abstract class com/apollographql/cache/gradle/ApolloCacheService { + public fun ()V + public abstract fun getPackageName ()Lorg/gradle/api/provider/Property; + public abstract fun getSchemaFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; +} + +public abstract class com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask : org/gradle/api/DefaultTask { + public fun ()V + public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getPackageName ()Lorg/gradle/api/provider/Property; + public abstract fun getSchemaFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun taskAction ()V +} + +public final class com/apollographql/cache/gradle/VersionKt { + public static final field VERSION Ljava/lang/String; +} + diff --git a/normalized-cache-gradle-plugin/build.gradle.kts b/normalized-cache-gradle-plugin/build.gradle.kts new file mode 100644 index 00000000..3223c824 --- /dev/null +++ b/normalized-cache-gradle-plugin/build.gradle.kts @@ -0,0 +1,27 @@ +import com.gradleup.librarian.gradle.librarianModule + +plugins { + alias(libs.plugins.kotlin.jvm) + id("java-gradle-plugin") +} + +librarianModule(true) + +dependencies { + compileOnly(libs.kgp.min) + compileOnly(libs.gradle.api.min) + implementation(libs.kotlin.poet) + implementation(libs.apollo.ast) + testImplementation(libs.kotlin.test) +} + +gradlePlugin { + plugins { + create("com.apollographql.cache") { + id = "com.apollographql.cache" + displayName = "com.apollographql.cache" + description = "Apollo Normalized Cache Gradle plugin" + implementationClass = "com.apollographql.cache.gradle.ApolloCachePlugin" + } + } +} diff --git a/normalized-cache-gradle-plugin/librarian.properties b/normalized-cache-gradle-plugin/librarian.properties new file mode 100644 index 00000000..64167950 --- /dev/null +++ b/normalized-cache-gradle-plugin/librarian.properties @@ -0,0 +1 @@ +version.packageName=com.apollographql.cache.gradle diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt new file mode 100644 index 00000000..3d627487 --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt @@ -0,0 +1,22 @@ +package com.apollographql.cache.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.ExternalDependency + +abstract class ApolloCachePlugin : Plugin { + override fun apply(target: Project) { + target.configureDefaultVersionsResolutionStrategy() + } + + private fun Project.configureDefaultVersionsResolutionStrategy() { + configurations.configureEach { configuration -> + configuration.withDependencies { dependencySet -> + val pluginVersion = VERSION + dependencySet.filterIsInstance() + .filter { it.group == "com.apollographql.cache" && it.version.isNullOrEmpty() } + .forEach { it.version { constraint -> constraint.require(pluginVersion) } } + } + } + } +} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt new file mode 100644 index 00000000..5ed8b3c2 --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt @@ -0,0 +1,49 @@ +package com.apollographql.cache.gradle + +import com.apollographql.cache.gradle.internal.codegen.Codegen +import com.apollographql.cache.gradle.internal.codegen.SchemaReader +import com.apollographql.cache.gradle.internal.codegen.toInputFiles +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +abstract class GenerateApolloCacheSourcesTask : DefaultTask() { + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val schemaFiles: ConfigurableFileCollection + + @get:Input + abstract val packageName: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + init { + outputDirectory.convention(project.layout.buildDirectory.dir("generated/source/apollo/cache")) + } + + @TaskAction + fun taskAction() { + val outputDir = outputDirectory.asFile.get() + outputDir.apply { + deleteRecursively() + mkdirs() + } + + val schemaReader = SchemaReader(schemaFiles.toInputFiles()) + val maxAges = schemaReader.getMaxAge() + Codegen( + packageName = packageName.get() + ".cache", + outputDirectory = outputDir, + maxAges = maxAges, + ) + .generate() + } +} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt new file mode 100644 index 00000000..24cb7af4 --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt @@ -0,0 +1,59 @@ +package com.apollographql.cache.gradle.internal.codegen + +import com.apollographql.cache.gradle.VERSION +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import java.io.File + +internal class Codegen( + private val packageName: String, + private val outputDirectory: File, + private val maxAges: Map, +) { + fun generate() { + generateCache() + } + + private fun generateCache() { + val initializer = CodeBlock.builder().apply { + add("mapOf(\n") + indent() + add( + maxAges.map { (k, v) -> + CodeBlock.of("%S to %L", k, v) + }.joinToString(",\n", postfix = ",\n") + ) + unindent() + add(")") + } + .build() + val file = FileSpec.builder(packageName, "Cache") + .addType( + TypeSpec.objectBuilder("Cache") + .addProperty( + PropertySpec.builder("maxAges", Map::class.asClassName() + .parameterizedBy(String::class.asClassName(), Int::class.asClassName()) + ) + .initializer(initializer) + .build() + ) + .build() + ) + .addFileComment( + """ + + AUTO-GENERATED FILE. DO NOT MODIFY. + + This class was automatically generated by Apollo GraphQL Cache version '$VERSION'. + + """.trimIndent() + ) + .build() + + file.writeTo(outputDirectory) + } +} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt new file mode 100644 index 00000000..e29b810f --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt @@ -0,0 +1,164 @@ +package com.apollographql.cache.gradle.internal.codegen + +import com.apollographql.apollo.annotations.ApolloExperimental +import com.apollographql.apollo.ast.GQLBooleanValue +import com.apollographql.apollo.ast.GQLDirective +import com.apollographql.apollo.ast.GQLDocument +import com.apollographql.apollo.ast.GQLIntValue +import com.apollographql.apollo.ast.GQLInterfaceTypeDefinition +import com.apollographql.apollo.ast.GQLObjectTypeDefinition +import com.apollographql.apollo.ast.GQLSchemaDefinition +import com.apollographql.apollo.ast.GQLStringValue +import com.apollographql.apollo.ast.GQLTypeDefinition +import com.apollographql.apollo.ast.Schema +import com.apollographql.apollo.ast.SourceLocation +import com.apollographql.apollo.ast.pretty +import com.apollographql.apollo.ast.toGQLDocument +import com.apollographql.apollo.ast.validateAsSchema +import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logging +import java.io.File + +private const val CACHE_CONTROL = "cacheControl" +private const val CACHE_CONTROL_FIELD = "cacheControlField" + +internal class SchemaReader( + private val schemaFiles: Collection, +) { + /* + * Taken from ApolloCompiler.buildCodegenSchema(), with error handling removed as errors will already be handled by it. + */ + private fun getSchema(): Schema { + val schemaDocuments = schemaFiles.map { + it.normalizedPath to it.file.toGQLDocument(allowJson = true) + } + // Locate the mainSchemaDocument. It's the one that contains the operation roots + val mainSchemaDocuments = mutableListOf() + val otherSchemaDocuments = mutableListOf() + schemaDocuments.forEach { + val document = it.second + if ( + document.definitions.filterIsInstance().isNotEmpty() + || document.definitions.filterIsInstance().any { it.name == "Query" } + ) { + mainSchemaDocuments.add(document) + } else { + otherSchemaDocuments.add(document) + } + } + val mainSchemaDocument = mainSchemaDocuments.single() + + // Sort the other schema document as type extensions are order sensitive + val otherSchemaDocumentSorted = otherSchemaDocuments.sortedBy { it.sourceLocation?.filePath?.substringAfterLast(File.pathSeparator) } + val schemaDefinitions = (listOf(mainSchemaDocument) + otherSchemaDocumentSorted).flatMap { it.definitions } + val schemaDocument = GQLDocument( + definitions = schemaDefinitions, + sourceLocation = null + ) + + @OptIn(ApolloExperimental::class) + val result = schemaDocument.validateAsSchema() + return result.value!! + } + + fun getMaxAge(): Map { + val schema = getSchema() + val typeDefinitions = schema.typeDefinitions + val issues = mutableListOf() + fun GQLDirective.maxAgeAndInherit(): Pair { + val maxAge = (arguments.firstOrNull { it.name == "maxAge" }?.value as? GQLIntValue)?.value?.toIntOrNull() + if (maxAge != null && maxAge < 0) { + issues += Issue("`maxAge` must not be negative", sourceLocation) + return null to false + } + val inheritMaxAge = (arguments.firstOrNull { it.name == "inheritMaxAge" }?.value as? GQLBooleanValue)?.value == true + if (maxAge == null && !inheritMaxAge || maxAge != null && inheritMaxAge) { + issues += Issue("`@$name` must either provide a `maxAge` or an `inheritMaxAge` set to true", sourceLocation) + return null to false + } + return maxAge to inheritMaxAge + } + + val maxAges = mutableMapOf() + for (typeDefinition in typeDefinitions.values) { + val typeCacheControlDirective = typeDefinition.directives.firstOrNull { schema.originalDirectiveName(it.name) == CACHE_CONTROL } + if (typeCacheControlDirective != null) { + val (maxAge, inheritMaxAge) = typeCacheControlDirective.maxAgeAndInherit() + if (maxAge != null) { + maxAges[typeDefinition.name] = maxAge + } else if (inheritMaxAge) { + maxAges[typeDefinition.name] = -1 + } + } + + val typeCacheControlFieldDirectives = + typeDefinition.directives.filter { schema.originalDirectiveName(it.name) == CACHE_CONTROL_FIELD } + for (fieldDirective in typeCacheControlFieldDirectives) { + val fieldName = (fieldDirective.arguments.first { it.name == "name" }.value as GQLStringValue).value + if (typeDefinition.fields.none { it.name == fieldName }) { + issues += Issue("Field `$fieldName` does not exist on type `${typeDefinition.name}`", fieldDirective.sourceLocation) + continue + } + val (maxAge, inheritMaxAge) = fieldDirective.maxAgeAndInherit() + if (maxAge != null) { + maxAges["${typeDefinition.name}.$fieldName"] = maxAge + } else if (inheritMaxAge) { + maxAges["${typeDefinition.name}.$fieldName"] = -1 + } + } + + for (field in typeDefinition.fields) { + val fieldCacheControlDirective = field.directives.firstOrNull { schema.originalDirectiveName(it.name) == CACHE_CONTROL } + if (fieldCacheControlDirective != null) { + val (maxAge, inheritMaxAge) = fieldCacheControlDirective.maxAgeAndInherit() + if (maxAge != null) { + maxAges["${typeDefinition.name}.${field.name}"] = maxAge + } else if (inheritMaxAge) { + maxAges["${typeDefinition.name}.${field.name}"] = -1 + } + } + } + } + if (issues.isNotEmpty()) { + for (issue in issues) { + issue.log() + } + throw IllegalStateException("Found issues in the schema") + } + return maxAges + } +} + +private class Issue( + val message: String, + val sourceLocation: SourceLocation?, +) { + fun log() { + Logging.getLogger("apollo").lifecycle("w: ${sourceLocation.pretty()}: Apollo: ${message}") + } +} + +/** + * An input file together with its normalizedPath + * normalizedPath is used to compute the package name in some cases + */ +internal class InputFile(val file: File, val normalizedPath: String) + +internal fun FileCollection.toInputFiles(): List { + val inputFiles = mutableListOf() + + asFileTree.visit { + if (it.file.isFile) { + inputFiles.add(InputFile(it.file, it.path)) + } + } + + return inputFiles +} + +private val GQLTypeDefinition.fields + get() = when (this) { + is GQLObjectTypeDefinition -> fields + is GQLInterfaceTypeDefinition -> fields + else -> emptyList() + } diff --git a/settings.gradle.kts b/settings.gradle.kts index d13d8bb5..2b7cb788 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,8 @@ pluginManagement { } } -include("normalized-cache-incubating", "normalized-cache-sqlite-incubating") +include( + "normalized-cache-incubating", + "normalized-cache-sqlite-incubating", + "normalized-cache-gradle-plugin", +) diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 2983a63a..0f83ed94 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -8,4 +8,5 @@ buildscript { plugins { alias(libs.plugins.kotlin.multiplatform).apply(false) alias(libs.plugins.apollo).apply(false) + alias(libs.plugins.apollo.cache).apply(false) } diff --git a/tests/expiration/build.gradle.kts b/tests/expiration/build.gradle.kts index 3d44afc9..4277ddd8 100644 --- a/tests/expiration/build.gradle.kts +++ b/tests/expiration/build.gradle.kts @@ -1,8 +1,10 @@ +import com.apollographql.cache.gradle.GenerateApolloCacheSourcesTask import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.apollo) + alias(libs.plugins.apollo.cache) } kotlin { @@ -73,3 +75,12 @@ apollo { srcDir("src/commonMain/graphql/declarative") } } + + +val generateDeclarativeApolloCacheSourcesTask = + tasks.register("generateDeclarativeApolloCacheSources", GenerateApolloCacheSourcesTask::class.java) { + packageName.set("declarative") + schemaFiles.from(file("src/commonMain/graphql/declarative")) + } + +kotlin.sourceSets.getByName("commonMain").kotlin.srcDir(generateDeclarativeApolloCacheSourcesTask) From 7db79e95911ac18999ace6a6284d6fdb95e880ea Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 26 Sep 2024 12:25:56 +0200 Subject: [PATCH 5/7] Add Gradle extension and register a task per service --- .../cache/gradle/ApolloCacheExtension.kt | 24 ++++++++ .../cache/gradle/ApolloCachePlugin.kt | 1 + .../cache/gradle/ApolloCacheService.kt | 18 ++++++ .../gradle/GenerateApolloCacheSourcesTask.kt | 4 -- .../cache/gradle/internal/kotlinProject.kt | 17 ++++++ tests/expiration/build.gradle.kts | 11 ++-- .../graphql/declarative/cache-v0.1.graphqls | 59 +++++++++++++++++++ 7 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheService.kt create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt create mode 100644 tests/expiration/src/commonMain/graphql/declarative/cache-v0.1.graphqls diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt new file mode 100644 index 00000000..5c02ca1e --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt @@ -0,0 +1,24 @@ +package com.apollographql.cache.gradle + +import com.apollographql.cache.gradle.internal.isKotlinMultiplatform +import com.apollographql.cache.gradle.internal.kotlinProjectExtensionOrThrow +import org.gradle.api.Action +import org.gradle.api.Project +import javax.inject.Inject + +abstract class ApolloCacheExtension @Inject constructor(val project: Project) { + fun service(serviceName: String, action: Action) { + val service = project.objects.newInstance(ApolloCacheService::class.java, serviceName) + action.execute(service) + + val task = + project.tasks.register("generate${serviceName.replaceFirstChar(Char::uppercase)}ApolloCacheSources", GenerateApolloCacheSourcesTask::class.java) { + it.packageName.set(service.packageName) + it.schemaFiles.from(service.graphqlSourceDirectorySet) + it.outputDirectory.set(project.layout.buildDirectory.dir("generated/source/apollo-cache/${serviceName}")) + } + + val mainSourceSetName = if (project.isKotlinMultiplatform) "commonMain" else "main" + project.kotlinProjectExtensionOrThrow.sourceSets.getByName(mainSourceSetName).kotlin.srcDir(task) + } +} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt index 3d627487..913a818c 100644 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCachePlugin.kt @@ -6,6 +6,7 @@ import org.gradle.api.artifacts.ExternalDependency abstract class ApolloCachePlugin : Plugin { override fun apply(target: Project) { + target.extensions.create("apolloCache", ApolloCacheExtension::class.java, target) target.configureDefaultVersionsResolutionStrategy() } diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheService.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheService.kt new file mode 100644 index 00000000..9b829edc --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheService.kt @@ -0,0 +1,18 @@ +package com.apollographql.cache.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +abstract class ApolloCacheService @Inject constructor( + private val name: String, + project: Project, +) { + abstract val packageName: Property + + internal val graphqlSourceDirectorySet = project.objects.sourceDirectorySet("graphql", "graphql") + + fun srcDir(directory: Any) { + graphqlSourceDirectorySet.srcDir(directory) + } +} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt index 5ed8b3c2..a087530c 100644 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt @@ -25,10 +25,6 @@ abstract class GenerateApolloCacheSourcesTask : DefaultTask() { @get:OutputDirectory abstract val outputDirectory: DirectoryProperty - init { - outputDirectory.convention(project.layout.buildDirectory.dir("generated/source/apollo/cache")) - } - @TaskAction fun taskAction() { val outputDir = outputDirectory.asFile.get() diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt new file mode 100644 index 00000000..8644315f --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt @@ -0,0 +1,17 @@ +package com.apollographql.cache.gradle.internal + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension + +internal val Project.isKotlinMultiplatform get() = pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform") + +internal val Project.kotlinMultiplatformExtension + get() = extensions.findByName("kotlin") as? KotlinMultiplatformExtension + +internal val Project.kotlinProjectExtension + get() = extensions.findByName("kotlin") as? KotlinProjectExtension + +internal val Project.kotlinProjectExtensionOrThrow + get() = kotlinProjectExtension + ?: throw IllegalStateException("Apollo: no 'kotlin' extension found. Did you apply the Kotlin Gradle plugin?") diff --git a/tests/expiration/build.gradle.kts b/tests/expiration/build.gradle.kts index 4277ddd8..c8576391 100644 --- a/tests/expiration/build.gradle.kts +++ b/tests/expiration/build.gradle.kts @@ -1,4 +1,3 @@ -import com.apollographql.cache.gradle.GenerateApolloCacheSourcesTask import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { @@ -76,11 +75,9 @@ apollo { } } - -val generateDeclarativeApolloCacheSourcesTask = - tasks.register("generateDeclarativeApolloCacheSources", GenerateApolloCacheSourcesTask::class.java) { +apolloCache { + service("declarative") { packageName.set("declarative") - schemaFiles.from(file("src/commonMain/graphql/declarative")) + srcDir("src/commonMain/graphql/declarative") } - -kotlin.sourceSets.getByName("commonMain").kotlin.srcDir(generateDeclarativeApolloCacheSourcesTask) +} diff --git a/tests/expiration/src/commonMain/graphql/declarative/cache-v0.1.graphqls b/tests/expiration/src/commonMain/graphql/declarative/cache-v0.1.graphqls new file mode 100644 index 00000000..e147971c --- /dev/null +++ b/tests/expiration/src/commonMain/graphql/declarative/cache-v0.1.graphqls @@ -0,0 +1,59 @@ +""" +Possible values for the `@cacheControl` `scope` argument (unused on the client). +""" +enum CacheControlScope { + PUBLIC + PRIVATE +} + +""" +Configures cache settings for a field or type. + +- `maxAge`: The maximum amount of time the field's cached value is valid, in seconds. +- `inheritMaxAge`: If true, the field inherits the `maxAge` of its parent field. If set to `true`, `maxAge` must not be provided. +- `scope`: Unused on the client. + +When applied to a type, the settings apply to all schema fields that return this type. +Field-level settings override type-level settings. + +For example: + +```graphql +type Query { +me: User +user(id: ID!): User @cacheControl(maxAge: 5) +} + +type User @cacheControl(maxAge: 10) { +id: ID! +email: String +} +``` +`Query.me` is valid for 10 seconds, and `Query.user` for 5 seconds. +""" +directive @cacheControl( + maxAge: Int + inheritMaxAge: Boolean + scope: CacheControlScope +) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION + +""" +Configures cache settings for a field. + +`@cacheControlField` is the same as `@cacheControl` but can be used on type system extensions for services that do not own the schema like +client services. + +For example: + +```graphql +# extend the schema to set a max age on User.email. +extend type User @cacheControlField(name: "email", maxAge: 20) +``` +`User.email` is valid for 20 seconds. +""" +directive @cacheControlField( + name: String! + maxAge: Int + inheritMaxAge: Boolean + scope: CacheControlScope +) repeatable on OBJECT | INTERFACE From 81e4009ff6e84ffab9392a2dd223c270c8cbee78 Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 26 Sep 2024 16:51:12 +0200 Subject: [PATCH 6/7] Generate a Map instead of Map --- .../cache/gradle/internal/codegen/Codegen.kt | 30 ++++++++++++++----- .../cache/normalized/api/MaxAgeProvider.kt | 12 -------- tests/expiration/build.gradle.kts | 2 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt index 24cb7af4..0bd1a212 100644 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/Codegen.kt @@ -1,13 +1,25 @@ package com.apollographql.cache.gradle.internal.codegen import com.apollographql.cache.gradle.VERSION +import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.STRING import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.asTypeName import java.io.File +import kotlin.time.Duration + +private object Symbols { + val MaxAge = ClassName("com.apollographql.cache.normalized.api", "MaxAge") + val MaxAgeInherit = MaxAge.nestedClass("Inherit") + val MaxAgeDuration = MaxAge.nestedClass("Duration") + val Seconds = MemberName(Duration.Companion::class.asTypeName(), "seconds", isExtension = true) +} internal class Codegen( private val packageName: String, @@ -22,11 +34,13 @@ internal class Codegen( val initializer = CodeBlock.builder().apply { add("mapOf(\n") indent() - add( - maxAges.map { (k, v) -> - CodeBlock.of("%S to %L", k, v) - }.joinToString(",\n", postfix = ",\n") - ) + maxAges.forEach { (field, duration) -> + if (duration == -1) { + addStatement("%S to %T,", field, Symbols.MaxAgeInherit) + } else { + addStatement("%S to %T(%L.%M),", field, Symbols.MaxAgeDuration, duration, Symbols.Seconds) + } + } unindent() add(")") } @@ -35,8 +49,8 @@ internal class Codegen( .addType( TypeSpec.objectBuilder("Cache") .addProperty( - PropertySpec.builder("maxAges", Map::class.asClassName() - .parameterizedBy(String::class.asClassName(), Int::class.asClassName()) + PropertySpec.builder("maxAges", MAP + .parameterizedBy(STRING, Symbols.MaxAge) ) .initializer(initializer) .build() diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt index bc3620f5..d205e3fe 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt @@ -3,7 +3,6 @@ package com.apollographql.cache.normalized.api import com.apollographql.apollo.api.CompiledField import com.apollographql.apollo.api.isComposite import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds interface MaxAgeProvider { /** @@ -114,14 +113,3 @@ class SchemaCoordinatesMaxAgeProvider( } } } - -fun SchemaCoordinatesMaxAgeProvider(maxAges: Map, defaultMaxAge: Duration): MaxAgeProvider { - val mappedMaxAges = maxAges.mapValues { - if (it.value == -1) { - MaxAge.Inherit - } else { - MaxAge.Duration(it.value.seconds) - } - } - return SchemaCoordinatesMaxAgeProvider(mappedMaxAges, defaultMaxAge) -} \ No newline at end of file diff --git a/tests/expiration/build.gradle.kts b/tests/expiration/build.gradle.kts index c8576391..e6b96149 100644 --- a/tests/expiration/build.gradle.kts +++ b/tests/expiration/build.gradle.kts @@ -38,6 +38,7 @@ kotlin { getByName("commonMain") { dependencies { implementation(libs.apollo.runtime) + implementation("com.apollographql.cache:normalized-cache-sqlite-incubating") } } @@ -46,7 +47,6 @@ kotlin { implementation(libs.apollo.testing.support) implementation(libs.apollo.mockserver) implementation(libs.kotlin.test) - implementation("com.apollographql.cache:normalized-cache-sqlite-incubating") } } From ea5137abcab425c3cf88ae225b4dc854f0cdadd7 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 27 Sep 2024 07:37:12 +0200 Subject: [PATCH 7/7] Use Gratatouille --- gradle.properties | 2 + gradle/libs.versions.toml | 2 + .../api/normalized-cache-gradle-plugin.api | 20 +++++---- .../build.gradle.kts | 2 + .../cache/gradle/ApolloCacheExtension.kt | 13 +++--- .../gradle/GenerateApolloCacheSourcesTask.kt | 45 ------------------- .../gradle/internal/codegen/SchemaReader.kt | 22 +-------- .../internal/generateApolloCacheSources.kt | 26 +++++++++++ .../cache/gradle/internal/kotlinProject.kt | 4 -- 9 files changed, 51 insertions(+), 85 deletions(-) delete mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt create mode 100644 normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/generateApolloCacheSources.kt diff --git a/gradle.properties b/gradle.properties index d8b32194..595ef1f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ org.gradle.jvmargs=-Xmx8g android.useAndroidX=true + +ksp.useKSP2=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25979324..7d3223e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,3 +42,5 @@ atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin- sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } apollo = { id = "com.apollographql.apollo", version.ref = "apollo" } apollo-cache = { id = "com.apollographql.cache" } +ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.24" } +gratatouille = { id = "com.gradleup.gratatouille", version = "0.0.4" } diff --git a/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api b/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api index c1b9cb62..a8c361f0 100644 --- a/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api +++ b/normalized-cache-gradle-plugin/api/normalized-cache-gradle-plugin.api @@ -11,20 +11,22 @@ public abstract class com/apollographql/cache/gradle/ApolloCachePlugin : org/gra } public abstract class com/apollographql/cache/gradle/ApolloCacheService { - public fun ()V + public fun (Ljava/lang/String;Lorg/gradle/api/Project;)V public abstract fun getPackageName ()Lorg/gradle/api/provider/Property; - public abstract fun getSchemaFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun srcDir (Ljava/lang/Object;)V +} + +public final class com/apollographql/cache/gradle/VersionKt { + public static final field VERSION Ljava/lang/String; } -public abstract class com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask : org/gradle/api/DefaultTask { +public final class com/apollographql/cache/gradle/internal/GenerateApolloCacheSourcesEntryPoint { + public static final field Companion Lcom/apollographql/cache/gradle/internal/GenerateApolloCacheSourcesEntryPoint$Companion; public fun ()V - public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty; - public abstract fun getPackageName ()Lorg/gradle/api/provider/Property; - public abstract fun getSchemaFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; - public final fun taskAction ()V + public static final fun run (Ljava/util/List;Ljava/lang/String;Ljava/io/File;)V } -public final class com/apollographql/cache/gradle/VersionKt { - public static final field VERSION Ljava/lang/String; +public final class com/apollographql/cache/gradle/internal/GenerateApolloCacheSourcesEntryPoint$Companion { + public final fun run (Ljava/util/List;Ljava/lang/String;Ljava/io/File;)V } diff --git a/normalized-cache-gradle-plugin/build.gradle.kts b/normalized-cache-gradle-plugin/build.gradle.kts index 3223c824..e9e5915c 100644 --- a/normalized-cache-gradle-plugin/build.gradle.kts +++ b/normalized-cache-gradle-plugin/build.gradle.kts @@ -3,6 +3,8 @@ import com.gradleup.librarian.gradle.librarianModule plugins { alias(libs.plugins.kotlin.jvm) id("java-gradle-plugin") + alias(libs.plugins.ksp) + alias(libs.plugins.gratatouille) } librarianModule(true) diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt index 5c02ca1e..6bf7df28 100644 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/ApolloCacheExtension.kt @@ -2,6 +2,7 @@ package com.apollographql.cache.gradle import com.apollographql.cache.gradle.internal.isKotlinMultiplatform import com.apollographql.cache.gradle.internal.kotlinProjectExtensionOrThrow +import com.apollographql.cache.gradle.internal.registerGenerateApolloCacheSourcesTask import org.gradle.api.Action import org.gradle.api.Project import javax.inject.Inject @@ -11,13 +12,11 @@ abstract class ApolloCacheExtension @Inject constructor(val project: Project) { val service = project.objects.newInstance(ApolloCacheService::class.java, serviceName) action.execute(service) - val task = - project.tasks.register("generate${serviceName.replaceFirstChar(Char::uppercase)}ApolloCacheSources", GenerateApolloCacheSourcesTask::class.java) { - it.packageName.set(service.packageName) - it.schemaFiles.from(service.graphqlSourceDirectorySet) - it.outputDirectory.set(project.layout.buildDirectory.dir("generated/source/apollo-cache/${serviceName}")) - } - + val task = project.registerGenerateApolloCacheSourcesTask( + taskName = "generate${serviceName.replaceFirstChar(Char::uppercase)}ApolloCacheSources", + schemaFiles = service.graphqlSourceDirectorySet, + packageName = service.packageName + ) val mainSourceSetName = if (project.isKotlinMultiplatform) "commonMain" else "main" project.kotlinProjectExtensionOrThrow.sourceSets.getByName(mainSourceSetName).kotlin.srcDir(task) } diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt deleted file mode 100644 index a087530c..00000000 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/GenerateApolloCacheSourcesTask.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.apollographql.cache.gradle - -import com.apollographql.cache.gradle.internal.codegen.Codegen -import com.apollographql.cache.gradle.internal.codegen.SchemaReader -import com.apollographql.cache.gradle.internal.codegen.toInputFiles -import org.gradle.api.DefaultTask -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -abstract class GenerateApolloCacheSourcesTask : DefaultTask() { - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val schemaFiles: ConfigurableFileCollection - - @get:Input - abstract val packageName: Property - - @get:OutputDirectory - abstract val outputDirectory: DirectoryProperty - - @TaskAction - fun taskAction() { - val outputDir = outputDirectory.asFile.get() - outputDir.apply { - deleteRecursively() - mkdirs() - } - - val schemaReader = SchemaReader(schemaFiles.toInputFiles()) - val maxAges = schemaReader.getMaxAge() - Codegen( - packageName = packageName.get() + ".cache", - outputDirectory = outputDir, - maxAges = maxAges, - ) - .generate() - } -} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt index e29b810f..fe7b05ee 100644 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/codegen/SchemaReader.kt @@ -15,7 +15,7 @@ import com.apollographql.apollo.ast.SourceLocation import com.apollographql.apollo.ast.pretty import com.apollographql.apollo.ast.toGQLDocument import com.apollographql.apollo.ast.validateAsSchema -import org.gradle.api.file.FileCollection +import gratatouille.FileWithPath import org.gradle.api.logging.Logging import java.io.File @@ -23,7 +23,7 @@ private const val CACHE_CONTROL = "cacheControl" private const val CACHE_CONTROL_FIELD = "cacheControlField" internal class SchemaReader( - private val schemaFiles: Collection, + private val schemaFiles: Collection, ) { /* * Taken from ApolloCompiler.buildCodegenSchema(), with error handling removed as errors will already be handled by it. @@ -138,24 +138,6 @@ private class Issue( } } -/** - * An input file together with its normalizedPath - * normalizedPath is used to compute the package name in some cases - */ -internal class InputFile(val file: File, val normalizedPath: String) - -internal fun FileCollection.toInputFiles(): List { - val inputFiles = mutableListOf() - - asFileTree.visit { - if (it.file.isFile) { - inputFiles.add(InputFile(it.file, it.path)) - } - } - - return inputFiles -} - private val GQLTypeDefinition.fields get() = when (this) { is GQLObjectTypeDefinition -> fields diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/generateApolloCacheSources.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/generateApolloCacheSources.kt new file mode 100644 index 00000000..f1f9cb10 --- /dev/null +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/generateApolloCacheSources.kt @@ -0,0 +1,26 @@ +package com.apollographql.cache.gradle.internal + +import com.apollographql.cache.gradle.internal.codegen.Codegen +import com.apollographql.cache.gradle.internal.codegen.SchemaReader +import gratatouille.GInputFiles +import gratatouille.GOutputDirectory +import gratatouille.GTaskAction + +@GTaskAction +internal fun generateApolloCacheSources( + schemaFiles: GInputFiles, + packageName: String, + outputDirectory: GOutputDirectory, +) { + outputDirectory.deleteRecursively() + outputDirectory.mkdirs() + + val schemaReader = SchemaReader(schemaFiles) + val maxAges = schemaReader.getMaxAge() + Codegen( + packageName = packageName + ".cache", + outputDirectory = outputDirectory, + maxAges = maxAges, + ) + .generate() +} diff --git a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt index 8644315f..eaae6222 100644 --- a/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt +++ b/normalized-cache-gradle-plugin/src/main/kotlin/com/apollographql/cache/gradle/internal/kotlinProject.kt @@ -1,14 +1,10 @@ package com.apollographql.cache.gradle.internal import org.gradle.api.Project -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension internal val Project.isKotlinMultiplatform get() = pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform") -internal val Project.kotlinMultiplatformExtension - get() = extensions.findByName("kotlin") as? KotlinMultiplatformExtension - internal val Project.kotlinProjectExtension get() = extensions.findByName("kotlin") as? KotlinProjectExtension