Skip to content

Code generators (model, client, response, serializers)#4

Open
halotukozak wants to merge 2 commits intofeat/model-parserfrom
feat/generators
Open

Code generators (model, client, response, serializers)#4
halotukozak wants to merge 2 commits intofeat/model-parserfrom
feat/generators

Conversation

@halotukozak
Copy link
Member

Summary

  • ModelGenerator — data classes, sealed interfaces, enums, type aliases from OpenAPI schemas
  • ClientGenerator — per-tag Ktor HTTP client classes with suspend functions
  • ApiResponseGenerator — sealed ApiResponse hierarchy (HttpSuccess, HttpError)
  • SerializersModuleGenerator — kotlinx.serialization module for polymorphic types
  • TypeMapping, NameUtils, Names, InlineSchemaDeduplicator utilities
  • Full test coverage (8 test files)

Test plan

  • ./gradlew core:test passes

Depends on #2

🤖 Generated with Claude Code

ModelGenerator, ClientGenerator, ApiResponseGenerator, SerializersModuleGenerator,
TypeMapping, NameUtils, Names, InlineSchemaDeduplicator with full test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 11, 2026 19:49
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

This PR introduces the core OpenAPI→Kotlin code generation pipeline: parsing + validation of specs into an intermediate model, then generating Kotlin model types, Ktor clients, response wrappers, and serializers wiring—backed by comprehensive tests and fixtures.

Changes:

  • Added SpecParser + SpecValidator and expanded test fixtures for refs, polymorphism, anyOf/oneOf, and Swagger v2 conversion.
  • Implemented generators for models (ModelGenerator), clients (ClientGenerator), API responses (ApiResponseGenerator), and polymorphic serializers module (SerializersModuleGenerator), plus supporting utilities.
  • Added extensive unit tests across parsing, naming, type mapping, and code generation behavior.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
core/build.gradle.kts Adds Arrow dependency and enables -Xcontext-parameters; pins ktlint version.
core/src/main/kotlin/com/avsystem/justworks/core/Generator.kt Adds generator entry point placeholder.
core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiResponseGenerator.kt Generates HttpError/HttpErrorType and HttpSuccess<T> KotlinPoet files.
core/src/main/kotlin/com/avsystem/justworks/core/gen/ClientGenerator.kt Generates per-tag Ktor client classes and endpoint suspend functions.
core/src/main/kotlin/com/avsystem/justworks/core/gen/InlineSchemaDeduplicator.kt Implements inline schema structural deduplication and collision handling.
core/src/main/kotlin/com/avsystem/justworks/core/gen/ModelGenerator.kt Generates models (data classes, sealed interfaces, type aliases), polymorphic serializers, and inline schema classes.
core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt Provides casing/name utilities and Kotlin keyword escaping.
core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt Centralizes KotlinPoet ClassName/MemberName references used by generators.
core/src/main/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGenerator.kt Generates SerializersModule registrations for sealed hierarchies.
core/src/main/kotlin/com/avsystem/justworks/core/gen/TypeMapping.kt Maps intermediate TypeRef to KotlinPoet TypeName.
core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt Defines intermediate API model (spec/endpoints/schemas/enums/type refs).
core/src/main/kotlin/com/avsystem/justworks/core/model/TypeRef.kt Defines TypeRef hierarchy and primitive types.
core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt Parses OpenAPI/Swagger into intermediate model; handles combinators/refs/wrapper patterns.
core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecValidator.kt Validates parsed OpenAPI input and surfaces issues/warnings.
core/src/test/kotlin/com/avsystem/justworks/core/gen/ApiResponseGeneratorTest.kt Tests for HttpError/HttpSuccess code generation.
core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt Tests client generation (methods, params, auth header, error handling, polymorphism wiring).
core/src/test/kotlin/com/avsystem/justworks/core/gen/InlineSchemaDedupTest.kt Tests inline schema deduplication and name collision behavior.
core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorPolymorphicTest.kt Tests oneOf/anyOf/discriminator handling, polymorphic serializer generation, and allOf interactions.
core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorTest.kt Tests model generation (data classes, enums, defaults, keyword escaping, type aliases).
core/src/test/kotlin/com/avsystem/justworks/core/gen/NameUtilsTest.kt Tests casing, enum constant naming, Kotlin identifier escaping, and operation naming.
core/src/test/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGeneratorTest.kt Tests serializers module generation for sealed hierarchies.
core/src/test/kotlin/com/avsystem/justworks/core/gen/TypeMappingTest.kt Tests TypeRef → KotlinPoet type mapping.
core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserPolymorphicTest.kt Tests parsing behavior for polymorphic and allOf specs.
core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt Tests core parsing, $ref resolution, error reporting, anyOf rules, and v2 conversion.
core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecValidatorTest.kt Tests validator behavior for missing required info.
core/src/test/resources/anyof-spec.yaml Fixture for anyOf parsing/generation behavior.
core/src/test/resources/anyof-valid-spec.yaml Fixture for anyOf + discriminator parsing behavior.
core/src/test/resources/invalid-spec.yaml Fixture for validator/parser failure behavior.
core/src/test/resources/mixed-combinator-spec.yaml Fixture ensuring mixed combinators raise an error.
core/src/test/resources/petstore-v2.json Swagger v2 fixture for conversion testing.
core/src/test/resources/petstore.yaml OpenAPI v3 fixture for baseline parsing tests.
core/src/test/resources/polymorphic-spec.yaml Polymorphism fixture (oneOf, discriminator, allOf).
core/src/test/resources/refs-spec.yaml Fixture for exercising various $ref resolution patterns.

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

Comment on lines +123 to +128
val builder = CodeBlock
.builder()
.add("%T·{\n", HTTP_CLIENT)
.indent()
.add("install(%T)·{\n", CONTENT_NEGOTIATION)
.indent()
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 templates in buildClientInitializer contain literal '·' characters (e.g., "%T·{", "expectSuccess·=·false"). These will be emitted into the generated Kotlin source and will not compile; use normal spaces/newlines and let KotlinPoet handle indentation instead of embedding this marker character.

Copilot uses AI. Check for mistakes.
Comment on lines +334 to +339
builder.addStatement(
"%S·in·element.%M -> %T.serializer()",
uniqueField,
JSON_OBJECT_EXT,
variantClassName,
)
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 generated when branch uses literal '·' characters in the statement format string ("%S·in·element..."), which will be emitted into the generated Kotlin code and cause compilation failures. Remove these marker characters and rely on KotlinPoet formatting/indentation.

Copilot uses AI. Check for mistakes.
Comment on lines +340 to +347
} else {
builder.addStatement(
"// No unique discriminating fields found for variant '$variantName'",
)
builder.addStatement(
"else -> TODO(%S)",
"No unique discriminating fields found for variant '$variantName' of anyOf '$parentName' - manual selectDeserializer required",
)
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.

else -> TODO(...) is emitted inside the loop over variants, which can produce multiple else branches in the same when (invalid Kotlin) and also makes later variant checks unreachable. Emit variant-specific conditions in the loop and add a single final else branch after the loop (either TODO or a SerializationException).

Copilot uses AI. Check for mistakes.
}
val compileKotlin: KotlinCompile by tasks
compileKotlin.compilerOptions {
freeCompilerArgs.set(listOf("-Xcontext-parameters"))
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.

freeCompilerArgs.set(listOf("-Xcontext-parameters")) overwrites any existing compiler args configured by other plugins/tasks. Prefer appending (e.g., add) to preserve defaults and avoid surprising build changes as the build grows.

Suggested change
freeCompilerArgs.set(listOf("-Xcontext-parameters"))
freeCompilerArgs.add("-Xcontext-parameters")

Copilot uses AI. Check for mistakes.
code.addStatement(
"append(%T.Authorization, %P)",
HTTP_HEADERS,
CodeBlock.of($$"Bearer ${'$'}{$tokenProvider}"),
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 header value CodeBlock uses $$"..." which is not valid Kotlin syntax, so the generator itself will not compile. Build the "Bearer " string using standard Kotlin string/CodeBlock formatting, and reference the tokenProvider property correctly (e.g., as a lambda invocation if that's the intent).

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

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +70
FunSpec
.constructorBuilder()
.addParameter("baseUrl", STRING)
.addParameter("tokenProvider", STRING)
.build(),
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.

tokenProvider is modeled as a String, but its name and usage imply it should provide a token dynamically (e.g., () -> String or suspend () -> String). Keeping it as a String makes token refresh impossible and is likely to produce incorrect Authorization headers for long-lived clients.

Copilot uses AI. Check for mistakes.
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import java.io.File
import kotlin.time.Instant
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 imported here, but Kotlin doesn't provide an Instant type in kotlin.time (Instant is in kotlinx-datetime / java.time). This import will break compilation; use kotlinx.datetime.Instant (or fully qualify) for the parse-time validation logic.

Suggested change
import kotlin.time.Instant
import kotlinx.datetime.Instant

Copilot uses AI. Check for mistakes.
Comment on lines +539 to +544
PrimitiveType.DATE_TIME -> {
// Validate at generation time
try {
Instant.parse(prop.defaultValue as String)
"kotlin.time.Instant.parse(\"${prop.defaultValue}\")"
} catch (e: Exception) {
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.

For DATE_TIME defaults, the generator both validates with Instant.parse(...) and emits kotlin.time.Instant.parse(...) into generated code. This will not compile and also doesn't match the repo's datetime dependency (kotlinx-datetime). Use kotlinx.datetime.Instant.parse(...) consistently for validation and emitted default values.

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +163
isPrimitiveOnly(schema) -> {
// For primitive-only schemas, generate type alias
// TODO: Extend SchemaModel to include primitiveType field for primitive-only schemas
// For now, defaulting to String as the most common case
listOf(generateTypeAlias(schema, STRING))
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.

Primitive-only schemas are currently emitted as typealias <Name> = String regardless of the actual OpenAPI primitive type/format. This will generate incorrect models for type: integer/number/boolean/.... Either extend the parsed model to retain the primitive type for such schemas or avoid type-alias generation until that information is available.

Copilot uses AI. Check for mistakes.

"array" -> items?.toTypeRef()?.let(TypeRef::Array) ?: TypeRef.Primitive(PrimitiveType.STRING)

"object" -> (additionalProperties as? Schema<*>)?.toTypeRef()
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.

OpenAPI type: object with additionalProperties represents a map, but this branch returns the value type directly instead of wrapping it as TypeRef.Map(...). This loses the map structure and will cause the generator to treat maps as their value type; return TypeRef.Map(valueType) here.

Suggested change
"object" -> (additionalProperties as? Schema<*>)?.toTypeRef()
"object" -> (additionalProperties as? Schema<*>)?.toTypeRef()?.let(TypeRef::Map)

Copilot uses AI. Check for mistakes.
@halotukozak halotukozak changed the base branch from master to feat/model-parser March 11, 2026 19:55
@halotukozak halotukozak changed the base branch from feat/model-parser to master March 12, 2026 14:17
@halotukozak halotukozak changed the base branch from master to feat/model-parser 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