Building a convention plugin to contain some commo...
# apollo-kotlin
s
Building a convention plugin to contain some common logic for two modules which need the apollo plugin applied. Rn it looks smth like this:
Copy code
class ApolloConventionPlugin : Plugin<Project> {
  override fun apply(target: Project) {
    with(target) {
      val libs = the<LibrariesForLibs>()
      with(pluginManager) { apply(libs.plugins.apollo.get().pluginId) }
      ...
      tasks.register("downloadApolloSchemasFromIntrospection") {
        dependsOn(tasks.withType<com.apollographql.apollo3.gradle.internal.ApolloDownloadSchemaTask>())
      }
    }
  }
}
but interestingly, when I run
./gradlew downloadApolloSchemasFromIntrospection
I get the error:
Copy code
A problem was found with the configuration of task ':apollo:giraffe:downloadApolloSchema' (type 'ApolloDownloadSchemaTask').
  - In plugin 'com.apollographql.apollo3' type 'com.apollographql.apollo3.gradle.internal.ApolloDownloadSchemaTask' property 'schema' doesn't have a configured value.
Yet the schemas are downloaded perfectly fine, even though both
apollo:module1:downloadApolloSchema
and
apollo:module2:downloadApolloSchema
says that it failed for both. My assumption here is that there’s multiple tasks of type ApolloDownloadSchemaTask, and I want to run the task which uses the type generated in DefaultApolloExtension instead maybe? Any thoughts why the task says it fails but it doesn’t actually. Or if I can somehow target specifically that task, so that it doesn’t run the generic
downloadApolloSchema
I think optimally I’d want it to run
apollo:module1:downloadService1ApolloSchemaFromIntrospection
and
apollo:module2:downloadService2ApolloSchemaFromIntrospection
instead
I want this convention plugin basically to have a single place to put this task in
Copy code
tasks.withType<com.apollographql.apollo3.gradle.internal.ApolloDownloadSchemaTask>().configureEach {
  doLast {
    val schemaPath = schema.get()
    val schemaFile = file(schemaPath)
    val textWithoutDoubleLineBreaks = schemaFile.readText().replace("\n\n", "\n")
    schemaFile.writeText(textWithoutDoubleLineBreaks)
  }
}
and to give a simple task to run once, both for CI to only have to call 1 gradle task, and to make it easier for my colleagues instead of running two things at the same time. So if there’s some existing task that downloads all those schemas that I am missing I’d love to just use that instead.
b
ah yeah I guess
tasks.withType<com.apollographql.apollo3.gradle.internal.ApolloDownloadSchemaTask>()
returns the one that's configured explicitly, and the generic one that expects to pass a schema as a parameter...
m
Depend on the names instead?
s
Yeah, that’s exactly what seems to happen here. I do wonder how it does end up dowloading the schema in the end though 😅 Along with wondering if there’s something I can do in this convention plugin. I think for now I will in each of the modules do this:
Copy code
tasks.register("downloadApolloSchemasFromIntrospection") {
  dependsOn(tasks.withName("downloadServiceNameApolloSchemaFromIntrospection"))
}
and call top level
downloadApolloSchemasFromIntrospection
as it should call both of them
Thing is if I set this up in the convention plugin, if I specify both names there, this will mean that this task which downloads both schemas will be registered for both modules, meaning that I will download them twice if both tasks are run. And I would optimally like this to live in the convention plugin and not the individual module, but I atm don’t see a way to do this 👀
m
The typical answer I would do is "wrap the apollo plugin" in your convention plugin (see also https://github.com/apollographql/apollo-kotlin/issues/4720)
"declarative" works well once the task graph is created. The tasks can get properties lazily and it all works fine. But adding dependencies between tasks is technically "graph creation"
Then there's also the simple fix:
Copy code
tasks.withType<com.apollographql.apollo3.gradle.internal.ApolloDownloadSchemaTask>().configureEach {
  doLast {
    if (schema.isPresent) {
      val schemaPath = schema.get()
      val schemaFile = file(schemaPath)
      val textWithoutDoubleLineBreaks = schemaFile.readText().replace("\n\n", "\n")
      schemaFile.writeText(textWithoutDoubleLineBreaks)
    }
  }
}
s
Aha yeah that’s interesting. Taking this a step further and creating our own wrapper. I think I will consider this if we want even more common logic and/or I want to add even more costumization for some reason. I haven’t done that before myself though so I am a bit afraid I’ll end up spending the entire day learning how to do that, so gonna leave it for some time where I wanna experiment 👨‍🔬🧪 For now, I’ll do this simple and dumb thing: For modules with name giraffe + octopus and their services with the same name: apollo:octopus
Copy code
tasks.register("downloadApolloSchemasFromIntrospection") {
  dependsOn(tasks.findByName("downloadOctopusApolloSchemaFromIntrospection"))
}
apollo:giraffe
Copy code
tasks.register("downloadApolloSchemasFromIntrospection") {
  dependsOn(tasks.findByName("downloadGiraffeApolloSchemaFromIntrospection"))
}
And then just call top level
downloadApolloSchemasFromIntrospection
on CI and it seems to work fine.
m
Yup, that'd work!
s
Oh, the line cutting part works just fine actually in the convention plugin. And now that you mention it it probably applies twice but it seems to work fine, I don’t think I am worried about that. Mostly for the download schema task to work in the first place.
m
Ah yea, scratch that, checking for
isPresent
is not enough because adding the CLI task as a dependency will try to execute it
You could disable it based on
schema.isPresent
but that's not great either
So your solution is good 👍
Copy code
tasks.register("downloadApolloSchemasFromIntrospection") {
  dependsOn(tasks.findByName("downloadOctopusApolloSchemaFromIntrospection"))
}
I also like that it puts all the graph creation closer together
Random thoughts: 1. We should expose APIs for users to consume the downloaded schema like we have for
outputDirConnection{}
. Using
ApolloDownloadSchemaTask
works but isn't great because it's "internal"
2. We should ship a proper CLI instead of relying on Gradle to do some CLI stuff
s
2. We should ship a proper CLI instead of relying on Gradle to do some CLI stuff
What would those things be? I find myself comfortable with handling most things with gradle lately, especially with the more familiar I get with gradle itself. How would this world with this CLI look like?
m
Copy code
$ brew install apollok (apollo kotlin cli)
$ apollok download --endpoint "https://..." --schema schema.graphqls
$ apollok validate --schema schema.graphqls --operation operation.graphl
$ apollok codegen --schema schema.graphqls --operation operation.graphl --out outdir
$ apollok stats --schema schema.graphqls --operation operation.graphl
$ apollok diff --schema1 schema1.graphqls --schema2 schema2.graphqls
Sky is the limit
That would not remove Gradle
downloadOctopusApolloSchemaFromIntrospection
but that would remove Gradle
downloadApolloSchema
that is only to be used from the CLI (and which causes your original unset property issue)
s
Aha yes! This makes a lot of sense now. Yeah I think that’d be an improvement. The current task
downloadApolloSchema
is something I’ve never used nor will use as I like to do this through gradle instead. I really am not a CLI type of person, I like the autocompletion that gradle gives me and all those guarantees that I am not messing up as I typically do with CLIs 😅
m
Yea it's very awkwayd. Also any path you path pass (😅 ) to gradle is resolved relative to the project dir, not cwd so it's super confusing
s
Oh yeah I’ve just done this deal with myself where I always opt to use any non-cli approach when I get the chance to do so. Insomnia/Postman instead of curl, IntelliJ/AS git integration instead of git from the terminal (for 99% of the things) and so on 😅
BTW I did this in the convention-plugin:
Copy code
tasks.register("downloadApolloSchemasFromIntrospection") {
  tasks.findByName("downloadGiraffeApolloSchemaFromIntrospection")?.let { downloadTask ->
    dependsOn(downloadTask)
  }
  tasks.findByName("downloadOctopusApolloSchemaFromIntrospection")?.let { downloadTask ->
    dependsOn(downloadTask)
  }
}
And this also works. Not even sure I like it, but at least it keeps the gradle.kts file of each individual module smaller and has the convention plugin contain this kind of logic. Which I typically like but now the convention relies on the sub-modules and what service name they specify which is definitely not good 🫣 I’ll just stop overthinking this and let it be 😅
m
Insomnia/Postman instead of curl
I recently switched from Postman to okhttp in Kotlin scripts 🤓
s
Now that’s just the giga brain move 🧠 I guess
com.squareup.okhttp3:logging-interceptor:4.10.0
transitively brings in the rest of okhttp right?
m
Looks like it (I didn't really think that one though 😅 )
b
(By the way @Stylianos Gakis I think you've mentioned it before but not sure - out of curiosity why are you stripping the
\n\n
?)
s
It’s just that reading the schema feels easier in my eyes that way with less screen space and when on bigger fonts. I can read more with less screen space. That’s literally it, I unfortunately don’t have a proper reason other than that 😂 Pics for reference
b
oh all right - that's a valid one 👍
s
Yeah I am that type of person to not add too many empty lines, use 2 spaces instead of 4 and that kind of thing. I guess I got beef with whitespace 🔫
b
hahaha 😄 We indent with 2 spaces on Apollo Kotlin too (first time for me). I got used to it pretty fast.
m
FYI, I opened https://github.com/apollographql/apollo-kotlin/pull/4742 to make it possible to use the download task output without relying on internal APIs. It's 4.0 and I don't think we're going to backport to 3.x but feedbacks are welcome!
🦜 1
s
Good idea, yeah! Gonna try it out when 4.0 drops 😊 Which btw is there a roadmap on a rough estimate on when that’d be and what you’d like to be included in it? I think I’ve seen smth like that in the past but can’t find it rn
It's pretty much a moving target but having a better IntelliJ story is the big item for 4.x
Alphas should start late April, stable not before fall
s
Okay it was literally right there, I was looking inside the
docs
directory 😅 Thanks a lot 😊 I see the “Better support for Jetpack Compose” point there, is there any place where you elaborate a bit further on what this entails? What improvements you want to see there?
m
You'll need to do telekinesis with @bod for that, he started working on it this week and since then everyone started asking 😄
telepathy, not telekinesis 😅
Although that might also be interesting
s
I could do telekinesis to bring him to the desk, that should also work 😂
m
Good luck with that, nothing is moving in France at the moment, the whole country is on strike 😅
b
About ✏️ we don't have anything concrete yet, so far we were thinking about extensions to get responses as State, and possibly something to help with pagination. If you have ideas/feedback don't hesitate to participate to the brainstorming on this ticket ! 🙂
s
Ah awesome, will definitely participate if something comes to my mind!