Gradle plugin with multi-spec support#5
Conversation
JustworksPlugin, JustworksExtension, JustworksGenerateTask, JustworksSharedTypesTask, JustworksSpecConfiguration for multi-spec DSL. Functional tests included. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a dedicated Gradle plugin module to generate Kotlin clients/models from multiple OpenAPI specs, and extends the core parser/generators with polymorphism, inline-schema handling, and shared response types.
Changes:
- Introduces
pluginsubproject withjustworks { specs { ... } }multi-spec DSL and generation tasks (including shared types). - Expands core OpenAPI parsing/validation and codegen (oneOf/anyOf/allOf, inline schema dedup, serializers module, response wrappers).
- Adds extensive unit + functional test coverage and test resources for parser/codegen.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| settings.gradle.kts | Adds plugin subproject to the build. |
| .gitignore | Ignores Gradle/Kotlin/IDE build artifacts. |
| core/build.gradle.kts | Adds Arrow + compiler args; ktlint pin workaround. |
| core/src/main/kotlin/com/avsystem/justworks/core/Generator.kt | Adds placeholder generator entry point. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiResponseGenerator.kt | Generates shared HttpError/HttpSuccess types. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ClientGenerator.kt | Generates Ktor client code per tag + error handling. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/InlineSchemaDeduplicator.kt | Adds inline schema structural dedup + naming collision handling. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ModelGenerator.kt | Adds polymorphic + inline schema generation features and defaults. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt | Adds name transformations for identifiers/enum constants/ops. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt | Centralizes KotlinPoet references for generator output. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGenerator.kt | Emits generatedSerializersModule for polymorphic types. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/TypeMapping.kt | Maps TypeRef to KotlinPoet TypeNames. |
| core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt | Introduces parsed model layer (ApiSpec, SchemaModel, etc.). |
| core/src/main/kotlin/com/avsystem/justworks/core/model/TypeRef.kt | Adds TypeRef IR including inline schemas. |
| core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt | Implements parsing + validation + $ref + combinator handling. |
| core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecValidator.kt | Adds spec validation issues (errors + warnings). |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ApiResponseGeneratorTest.kt | Tests shared response type generation. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt | Tests generated client structure and request wiring. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/InlineSchemaDedupTest.kt | Tests inline schema dedup + naming collision behavior. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorPolymorphicTest.kt | Tests polymorphic model generation behaviors. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorTest.kt | Tests model generation incl defaults + keywords + aliases. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/NameUtilsTest.kt | Tests name/identifier conversion utilities. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGeneratorTest.kt | Tests serializers module generation output. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/TypeMappingTest.kt | Tests TypeRef -> Kotlin type mapping. |
| core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserPolymorphicTest.kt | Tests oneOf/allOf/discriminator parsing outcomes. |
| core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt | Tests parser correctness across refs/v2/anyOf errors. |
| core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecValidatorTest.kt | Tests validation issue detection. |
| core/src/test/resources/anyof-spec.yaml | Adds anyOf test fixture. |
| core/src/test/resources/anyof-valid-spec.yaml | Adds discriminator + anyOf test fixture. |
| core/src/test/resources/invalid-spec.yaml | Adds invalid spec fixture for error tests. |
| core/src/test/resources/mixed-combinator-spec.yaml | Adds mixed combinator fixture for failure tests. |
| core/src/test/resources/petstore-v2.json | Adds Swagger v2 conversion fixture. |
| core/src/test/resources/petstore.yaml | Adds OpenAPI v3 petstore fixture. |
| core/src/test/resources/polymorphic-spec.yaml | Adds oneOf/allOf polymorphism fixture. |
| core/src/test/resources/refs-spec.yaml | Adds $ref resolution fixture. |
| plugin/build.gradle.kts | Defines Gradle plugin + functionalTest source set. |
| plugin/src/functionalTest/kotlin/com/avsystem/justworks/gradle/JustworksPluginFunctionalTest.kt | End-to-end TestKit coverage for multi-spec plugin. |
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksExtension.kt | Adds multi-spec DSL extension container. |
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksGenerateTask.kt | Cacheable generation task wiring parser + generators. |
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksPlugin.kt | Registers tasks per spec + source-set wiring. |
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksSharedTypesTask.kt | Cacheable task to generate shared response types. |
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksSpecConfiguration.kt | Adds per-spec configuration object. |
Comments suppressed due to low confidence (2)
plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksSpecConfiguration.kt:1
- The KDoc DSL example uses
specFile = .../packageName = ..., but these arevalGradlePropertytypes; assignment won’t compile in Kotlin DSL. Update the docs to usespecFile.set(file(...))andpackageName.set("...")(and similarly forapiPackage/modelPackage), or change the API to expose Kotlin-DSL-friendly setters (e.g.,var specFile: Filedelegating to the underlyingRegularFileProperty).
plugin/src/functionalTest/kotlin/com/avsystem/justworks/gradle/JustworksPluginFunctionalTest.kt:1 - The functional test project’s build script configures
specFile/packageNamevia assignment, butJustworksSpecConfigurationexposes these asRegularFileProperty/Property<String>vals, so the script should use.set(...)(or the plugin should provide DSL-friendly setters). As written, the TestKit build should fail during Kotlin script compilation.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
The CodeBlock.of($$"Bearer ...") expression is not valid Kotlin syntax and will not compile. Build the generated string template using a normal Kotlin string (escaping $ as needed) and/or KotlinPoet format placeholders (e.g., %P) so the output becomes a valid Kotlin string literal like "Bearer ${tokenProvider}" (or function call if tokenProvider is meant to be a provider).
| CodeBlock.of("%S", "Bearer ${'$'}{tokenProvider}"), |
There was a problem hiding this comment.
kotlin.time.Instant is not the same type used elsewhere in this repo for DATE_TIME (TypeMapping maps to kotlinx.datetime.Instant), and kotlin.time.Instant is not a generally available time API for parsing ISO instants. This will either fail to compile or generate code that doesn’t match the declared types; switch validation and emitted default expressions to kotlinx.datetime.Instant.parse(...) (and remove the incorrect import).
| import kotlinx.datetime.Instant |
There was a problem hiding this comment.
kotlin.time.Instant is not the same type used elsewhere in this repo for DATE_TIME (TypeMapping maps to kotlinx.datetime.Instant), and kotlin.time.Instant is not a generally available time API for parsing ISO instants. This will either fail to compile or generate code that doesn’t match the declared types; switch validation and emitted default expressions to kotlinx.datetime.Instant.parse(...) (and remove the incorrect import).
There was a problem hiding this comment.
This emits else -> ... inside the loop, which can generate multiple else branches and/or statements after an else branch—both produce invalid Kotlin. Generate only condition branches in the loop, then add exactly one final else -> ... after the loop (either a single TODO with a summary, or a SerializationException), so the when expression is syntactically valid for any number of variants.
| } | |
| } | |
| // Add exactly one final else clause: | |
| // - if some variants have no unique fields: TODO with a summary | |
| // - otherwise: SerializationException for unknown variant | |
| val variantsWithoutUniqueFields = uniqueFieldsPerVariant | |
| .filter { it.second == null } | |
| .map { it.first } | |
| if (variantsWithoutUniqueFields.isNotEmpty()) { | |
| val summary = variantsWithoutUniqueFields.joinToString { "'$it'" } | |
| builder.addStatement( | |
| "else -> TODO(%S)", | |
| "No unique discriminating fields found for variants $summary of anyOf '$parentName' - manual selectDeserializer required", | |
| ) | |
| } else { | |
| builder.addStatement( |
There was a problem hiding this comment.
JsonContentPolymorphicSerializer<T>.selectDeserializer must override a method returning DeserializationStrategy<T> (for T = <sealed type>). Returning DeserializationStrategy<*> is not a valid override and should fail compilation. Change the return type to DeserializationStrategy<${schema.name}> (or an equivalent KotlinPoet TypeName) so the override matches the base signature.
There was a problem hiding this comment.
The return ParseResult.Success(...) inside the either {} block is a non-local return and will short-circuit the outer function, which makes the surrounding either { ... }.merge() expression inconsistent/ill-typed (and is very likely to not compile). Instead, make the success value the last expression of the either block (no return), and adjust the either/merge usage so the error and success types align (e.g., return Either<ParseResult, ParseResult> and merge, or avoid either here and use explicit early returns).
There was a problem hiding this comment.
Several imports appear unused in this file (e.g., NonEmptyList, Raise, RaiseAccumulate, ensure, ensureNotNull, recover). If the project enforces ktlint/detekt rules, these may fail linting; remove unused imports or use them as intended.
| import arrow.core.raise.ExperimentalRaiseAccumulateApi | |
| import arrow.core.raise.context.accumulate | |
| import arrow.core.raise.context.ensureNotNullOrAccumulate | |
| import arrow.core.raise.context.ensureOrAccumulate | |
| import arrow.core.raise.fold |
There was a problem hiding this comment.
isPrimitiveOnly currently treats any schema with no properties/combinators as “primitive-only” and then hardcodes the alias target to String, which can silently generate incorrect models (e.g., empty-object schemas, free-form objects, numeric/string schemas with formats, etc.). Consider disabling this branch until the parser/model carries the underlying primitive/object type information, or extend SchemaModel to include the original schema kind so the alias target can be correct.
There was a problem hiding this comment.
isPrimitiveOnly currently treats any schema with no properties/combinators as “primitive-only” and then hardcodes the alias target to String, which can silently generate incorrect models (e.g., empty-object schemas, free-form objects, numeric/string schemas with formats, etc.). Consider disabling this branch until the parser/model carries the underlying primitive/object type information, or extend SchemaModel to include the original schema kind so the alias target can be correct.
| * | |
| * NOTE: This heuristic is intentionally disabled because it relied only on the absence | |
| * of properties/combinators and could misclassify empty-object or non-string schemas | |
| * as primitive-only. Once SchemaModel exposes the underlying schema kind/primitive | |
| * type, this method should be updated to use that information. | |
| */ | |
| private fun isPrimitiveOnly(schema: SchemaModel): Boolean { | |
| // Disabled until we have reliable primitive/object type information. | |
| return false | |
| } |
Summary
JustworksPlugin— registers tasks per spec configurationJustworksExtensionwithNamedDomainObjectContainerfor multi-spec DSLJustworksGenerateTask— wires parser + generators into Gradle taskJustworksSharedTypesTask— generates shared API response typesJustworksSpecConfiguration— per-spec configuration (specFile, packages, outputDir)Test plan
./gradlew plugin:testpasses./gradlew core:testpassesDepends on #4
🤖 Generated with Claude Code