Code generators (model, client, response, serializers)#4
Code generators (model, client, response, serializers)#4halotukozak wants to merge 2 commits intofeat/model-parserfrom
Conversation
ModelGenerator, ClientGenerator, ApiResponseGenerator, SerializersModuleGenerator, TypeMapping, NameUtils, Names, InlineSchemaDeduplicator with full test coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
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+SpecValidatorand 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.
| val builder = CodeBlock | ||
| .builder() | ||
| .add("%T·{\n", HTTP_CLIENT) | ||
| .indent() | ||
| .add("install(%T)·{\n", CONTENT_NEGOTIATION) | ||
| .indent() |
There was a problem hiding this comment.
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.
| builder.addStatement( | ||
| "%S·in·element.%M -> %T.serializer()", | ||
| uniqueField, | ||
| JSON_OBJECT_EXT, | ||
| variantClassName, | ||
| ) |
There was a problem hiding this comment.
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.
| } 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", | ||
| ) |
There was a problem hiding this comment.
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).
| } | ||
| val compileKotlin: KotlinCompile by tasks | ||
| compileKotlin.compilerOptions { | ||
| freeCompilerArgs.set(listOf("-Xcontext-parameters")) |
There was a problem hiding this comment.
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.
| freeCompilerArgs.set(listOf("-Xcontext-parameters")) | |
| freeCompilerArgs.add("-Xcontext-parameters") |
| code.addStatement( | ||
| "append(%T.Authorization, %P)", | ||
| HTTP_HEADERS, | ||
| CodeBlock.of($$"Bearer ${'$'}{$tokenProvider}"), |
There was a problem hiding this comment.
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).
| CodeBlock.of($$"Bearer ${'$'}{$tokenProvider}"), | |
| CodeBlock.of("\"Bearer ${'$'}{tokenProvider()}\""), |
| FunSpec | ||
| .constructorBuilder() | ||
| .addParameter("baseUrl", STRING) | ||
| .addParameter("tokenProvider", STRING) | ||
| .build(), |
There was a problem hiding this comment.
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.
| import com.squareup.kotlinpoet.TypeSpec | ||
| import com.squareup.kotlinpoet.asTypeName | ||
| import java.io.File | ||
| import kotlin.time.Instant |
There was a problem hiding this comment.
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.
| import kotlin.time.Instant | |
| import kotlinx.datetime.Instant |
| PrimitiveType.DATE_TIME -> { | ||
| // Validate at generation time | ||
| try { | ||
| Instant.parse(prop.defaultValue as String) | ||
| "kotlin.time.Instant.parse(\"${prop.defaultValue}\")" | ||
| } catch (e: Exception) { |
There was a problem hiding this comment.
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.
| 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)) |
There was a problem hiding this comment.
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.
|
|
||
| "array" -> items?.toTypeRef()?.let(TypeRef::Array) ?: TypeRef.Primitive(PrimitiveType.STRING) | ||
|
|
||
| "object" -> (additionalProperties as? Schema<*>)?.toTypeRef() |
There was a problem hiding this comment.
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.
| "object" -> (additionalProperties as? Schema<*>)?.toTypeRef() | |
| "object" -> (additionalProperties as? Schema<*>)?.toTypeRef()?.let(TypeRef::Map) |
Summary
ModelGenerator— data classes, sealed interfaces, enums, type aliases from OpenAPI schemasClientGenerator— per-tag Ktor HTTP client classes with suspend functionsApiResponseGenerator— sealedApiResponsehierarchy (HttpSuccess,HttpError)SerializersModuleGenerator— kotlinx.serialization module for polymorphic typesTypeMapping,NameUtils,Names,InlineSchemaDeduplicatorutilitiesTest plan
./gradlew core:testpassesDepends on #2
🤖 Generated with Claude Code