Code generators (model, client, response, serializers) - #4
Conversation
Gradle build configuration with CI
chore: add .editorconfig and integrate ktlint
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()}\""), |
| 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.
…usage - Replace `Closeable` with `ApiClientBase` to streamline client structure. - Switch to `safeCall` for standardized error handling. - Implement `toResult` and `toEmptyResult` for consistent response processing. - Refactor tests to reflect APIClient changes and shared helpers.
- Simplified schema handling in `ModelGenerator` and related test files. - Updated tests to reflect removal of `isEnum` for cleaner and more maintainable code. - Adjusted polymorphic schema generation to streamline logic and avoid redundant checks.
…licity - Removed redundant `primitiveTypeName` method by merging its logic directly into `toTypeName`. - Streamlined handling of `PrimitiveType` variants and improved code clarity.
…ions across core modules - Consolidated function parameter formatting into single lines where applicable for cleaner and more consistent code style. - Updated relevant test files and utilities to match revised formatting.
… clarity and consistency - Removed `Generator` placeholder object and unused tests for backtick-escaped keywords handling. - Replaced `generateSuccess` with `generateHttpSuccess` in `ApiResponseGenerator`. - Simplified string utilities in `NameUtils`, removing hardcoded keyword checks. - Streamlined schema and model generation logic in `ModelGenerator`. - Refactored related tests
…rove readability across core generators - Removed `generateTo` methods from `ApiResponseGenerator` and `ClientGenerator` for unused file-writing logic. - Simplified parameter grouping and processing in `ClientGenerator` using cleaner mappings and utility methods. - Consolidated inline schema resolution in `ModelGenerator` to use a worklist for more efficient and readable logic. - Adjusted formatting and applied constants for improved consistency across modules.
|
will be divided into several smaller PRs |
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