Skip to content

Gradle plugin with multi-spec support#5

Open
halotukozak wants to merge 3 commits intofeat/generatorsfrom
feat/plugin
Open

Gradle plugin with multi-spec support#5
halotukozak wants to merge 3 commits intofeat/generatorsfrom
feat/plugin

Conversation

@halotukozak
Copy link
Member

Summary

  • JustworksPlugin — registers tasks per spec configuration
  • JustworksExtension with NamedDomainObjectContainer for multi-spec DSL
  • JustworksGenerateTask — wires parser + generators into Gradle task
  • JustworksSharedTypesTask — generates shared API response types
  • JustworksSpecConfiguration — per-spec configuration (specFile, packages, outputDir)
  • Functional test suite

Test plan

  • ./gradlew plugin:test passes
  • ./gradlew core:test passes

Depends on #4

🤖 Generated with Claude Code

JustworksPlugin, JustworksExtension, JustworksGenerateTask,
JustworksSharedTypesTask, JustworksSpecConfiguration for multi-spec DSL.
Functional tests included.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 11, 2026 19:51
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@halotukozak halotukozak changed the base branch from master to feat/generators March 11, 2026 19:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 plugin subproject with justworks { 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 are val Gradle Property types; assignment won’t compile in Kotlin DSL. Update the docs to use specFile.set(file(...)) and packageName.set("...") (and similarly for apiPackage/modelPackage), or change the API to expose Kotlin-DSL-friendly setters (e.g., var specFile: File delegating to the underlying RegularFileProperty).
    plugin/src/functionalTest/kotlin/com/avsystem/justworks/gradle/JustworksPluginFunctionalTest.kt:1
  • The functional test project’s build script configures specFile/packageName via assignment, but JustworksSpecConfiguration exposes these as RegularFileProperty/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.

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
CodeBlock.of("%S", "Bearer ${'$'}{tokenProvider}"),

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
import kotlinx.datetime.Instant

Copilot uses AI. Check for mistakes.
Comment on lines 539 to 544
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 344 to 354
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}
}
// 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(

Copilot uses AI. Check for mistakes.
Comment on lines 293 to 302
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 39 to 64
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 3 to 13
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines 159 to 165
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change

Copilot uses AI. Check for mistakes.
Comment on lines 686 to 689
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
*
* 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
}

Copilot uses AI. Check for mistakes.
@halotukozak halotukozak changed the base branch from feat/generators to master March 12, 2026 14:17
@halotukozak halotukozak changed the base branch from master to feat/generators March 12, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants