From 5fb5812549032b0a808e071690228946c26e6787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 25 Mar 2026 13:54:46 +0100 Subject: [PATCH 1/6] docs: add comprehensive usage guide to README - Add Generated Client Usage section with dependencies, client creation, auth examples - Document Bearer, API Key, Basic, and no-auth constructor patterns - Add HttpResult/Either error handling guide with HttpError subtype table - Add serialization setup with SerializersModule for polymorphic types - Add multi-spec configuration example Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/README.md b/README.md index bf3e944..2d672a2 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,194 @@ Generated sources are automatically wired into Kotlin source sets, so `compileKo | `apiPackage` | No | `$packageName.api` | Package for API client classes | | `modelPackage` | No | `$packageName.model` | Package for model/data classes | +## Generated Client Usage + +After running code generation, the plugin produces type-safe Kotlin client classes. +Here is how to use them. + +### Dependencies + +Add the required runtime dependencies to your consuming project: + +```kotlin +dependencies { + implementation("io.ktor:ktor-client-core:3.1.1") + implementation("io.ktor:ktor-client-cio:3.1.1") // or another engine (OkHttp, Apache, etc.) + implementation("io.ktor:ktor-client-content-negotiation:3.1.1") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + implementation("io.arrow-kt:arrow-core:2.1.2") +} +``` + +### Creating the Client + +Each generated client extends `ApiClientBase` and creates its own pre-configured `HttpClient` internally. +You only need to provide the base URL and authentication credentials: + +```kotlin +val client = PetstoreApi( + baseUrl = "https://api.example.com", + token = { "your-bearer-token" }, +) +``` + +Auth parameters are lambdas (`() -> String`), so you can supply a token provider that refreshes automatically: + +```kotlin +val client = PetstoreApi( + baseUrl = "https://api.example.com", + token = { tokenStore.getAccessToken() }, +) +``` + +The client implements `Closeable` -- call `client.close()` when done to release HTTP resources. + +### Authentication + +The generated constructor signature depends on the security schemes defined in your OpenAPI spec: + +**Bearer Token** (single scheme): + +```kotlin +val client = PetstoreApi( + baseUrl = "https://api.example.com", + token = { "your-bearer-token" }, +) +``` + +**API Key** (sent as header or query parameter based on the spec): + +```kotlin +val client = PetstoreApi( + baseUrl = "https://api.example.com", + myApiKey = { "your-api-key" }, +) +``` + +**HTTP Basic**: + +```kotlin +val client = PetstoreApi( + baseUrl = "https://api.example.com", + myAuthUsername = { "user" }, + myAuthPassword = { "pass" }, +) +``` + +**No Authentication** (spec has no security schemes): + +```kotlin +val client = PetstoreApi( + baseUrl = "https://api.example.com", +) +``` + +When the spec defines multiple security schemes, the constructor includes a parameter for each one. + +### Making Requests + +Every endpoint becomes a `suspend` function on the client. The return type is `HttpResult`, where `E` is the error body type and `T` is the success body type: + +```kotlin +val result: HttpResult> = client.listPets(limit = 10) +``` + +Path, query, and header parameters map to function arguments. Optional parameters default to `null`: + +```kotlin +val result = client.findPets(status = "available", limit = 20) +``` + +### Error Handling + +`HttpResult` is a typealias for `Either, HttpSuccess>` (using [Arrow](https://arrow-kt.io/)). +Every API call returns a result instead of throwing exceptions: + +```kotlin +when (val result = client.getPet(petId = 123)) { + is Either.Right -> { + val pet = result.value.body + println("Found: ${pet.name}") + } + is Either.Left -> when (val error = result.value) { + is HttpError.NotFound -> println("Pet not found") + is HttpError.Unauthorized -> println("Auth required") + is HttpError.Network -> println("Connection failed: ${error.cause}") + else -> println("Error ${error.statusCode}: ${error.body}") + } +} +``` + +`HttpError` covers specific HTTP status codes as sealed subtypes: + +| Subtype | Status | +|------------------------|--------| +| `BadRequest` | 400 | +| `Unauthorized` | 401 | +| `Forbidden` | 403 | +| `NotFound` | 404 | +| `MethodNotAllowed` | 405 | +| `Conflict` | 409 | +| `Gone` | 410 | +| `UnprocessableEntity` | 422 | +| `TooManyRequests` | 429 | +| `InternalServerError` | 500 | +| `BadGateway` | 502 | +| `ServiceUnavailable` | 503 | +| `Network` | -- | +| `Other` | any | + +Network errors (connection timeouts, DNS failures) are caught and wrapped in `HttpError.Network` instead of propagating exceptions. + +### Serialization Setup + +Generated models use `@Serializable` from kotlinx.serialization. The client sets up JSON content negotiation internally via the `createHttpClient()` method. + +If your spec uses polymorphic types (`oneOf` / `anyOf` with discriminators), the generator produces a `SerializersModule` that is automatically registered with the internal JSON instance. No manual serialization configuration is needed. + +If you need to customize the JSON configuration for external use (e.g., parsing API responses outside the client), use the same settings: + +```kotlin +val json = Json { + ignoreUnknownKeys = true // recommended: specs may evolve + isLenient = true // optional: tolerant parsing +} +``` + +For polymorphic types, register the generated `SerializersModule`: + +```kotlin +val json = Json { + ignoreUnknownKeys = true + serializersModule = com.example.petstore.model.generatedSerializersModule +} +``` + +### Multi-Spec Configuration + +When your project consumes multiple APIs, register each spec separately. +The plugin generates independent client classes per spec: + +```kotlin +justworks { + specs { + register("petstore") { + specFile = file("api/petstore.yaml") + packageName = "com.example.petstore" + } + register("payments") { + specFile = file("api/payments.yaml") + packageName = "com.example.payments" + apiPackage = "com.example.payments.client" + modelPackage = "com.example.payments.dto" + } + } +} +``` + +Each spec gets its own Gradle task (`justworksGenerate`) and output directory. The generated clients are independent and can be used side by side. + ## Publishing Releases are published to [Maven Central](https://central.sonatype.com/) automatically when a version tag (`v*`) is pushed. The CD pipeline runs CI checks first, then publishes signed artifacts via the [vanniktech maven-publish](https://github.com/vanniktech/gradle-maven-publish-plugin) plugin. From 9008376d64bf1a1cf381e33fea636213d6338ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 25 Mar 2026 15:15:53 +0100 Subject: [PATCH 2/6] docs: address PR review comments on usage guide Fix inaccuracies in generated client documentation to match actual code: use tag-derived class names (PetsApi), Bearer-only auth, Raise-based error handling, HttpErrorType enum, and correct JSON config. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 118 ++++++++++++++++++++++-------------------------------- 1 file changed, 47 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 2d672a2..bc6638f 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,10 @@ dependencies { Each generated client extends `ApiClientBase` and creates its own pre-configured `HttpClient` internally. You only need to provide the base URL and authentication credentials: +Generated client class names are derived from OpenAPI tags as `Api` (e.g., a `pets` tag produces `PetsApi`). If the spec has no tags, the class is named `DefaultApi`. + ```kotlin -val client = PetstoreApi( +val client = PetsApi( baseUrl = "https://api.example.com", token = { "your-bearer-token" }, ) @@ -91,7 +93,7 @@ val client = PetstoreApi( Auth parameters are lambdas (`() -> String`), so you can supply a token provider that refreshes automatically: ```kotlin -val client = PetstoreApi( +val client = PetsApi( baseUrl = "https://api.example.com", token = { tokenStore.getAccessToken() }, ) @@ -101,52 +103,24 @@ The client implements `Closeable` -- call `client.close()` when done to release ### Authentication -The generated constructor signature depends on the security schemes defined in your OpenAPI spec: - -**Bearer Token** (single scheme): +The generated constructor always takes a `baseUrl: String` and a `token: () -> String` parameter. The `token` lambda is called on each request and its value is sent as a `Bearer` token in the `Authorization` header: ```kotlin -val client = PetstoreApi( +val client = PetsApi( baseUrl = "https://api.example.com", token = { "your-bearer-token" }, ) ``` -**API Key** (sent as header or query parameter based on the spec): - -```kotlin -val client = PetstoreApi( - baseUrl = "https://api.example.com", - myApiKey = { "your-api-key" }, -) -``` - -**HTTP Basic**: - -```kotlin -val client = PetstoreApi( - baseUrl = "https://api.example.com", - myAuthUsername = { "user" }, - myAuthPassword = { "pass" }, -) -``` - -**No Authentication** (spec has no security schemes): - -```kotlin -val client = PetstoreApi( - baseUrl = "https://api.example.com", -) -``` - -When the spec defines multiple security schemes, the constructor includes a parameter for each one. - ### Making Requests -Every endpoint becomes a `suspend` function on the client. The return type is `HttpResult`, where `E` is the error body type and `T` is the success body type: +Every endpoint becomes a `suspend` function on the client. Functions use Arrow's [Raise](https://arrow-kt.io/docs/typed-errors/) for structured error handling -- they require a `context(Raise)` and return `HttpSuccess` on success: ```kotlin -val result: HttpResult> = client.listPets(limit = 10) +// Inside a Raise context (e.g., within either { ... }) +val result: HttpSuccess> = client.listPets(limit = 10) +println(result.body) // the deserialized response body +println(result.code) // the HTTP status code ``` Path, query, and header parameters map to function arguments. Optional parameters default to `null`: @@ -157,44 +131,46 @@ val result = client.findPets(status = "available", limit = 20) ### Error Handling -`HttpResult` is a typealias for `Either, HttpSuccess>` (using [Arrow](https://arrow-kt.io/)). -Every API call returns a result instead of throwing exceptions: +Generated endpoints use [Arrow's Raise](https://arrow-kt.io/docs/typed-errors/) -- errors are raised, not returned as `Either`. Use Arrow's `either { ... }` block to obtain an `Either>`: ```kotlin -when (val result = client.getPet(petId = 123)) { - is Either.Right -> { - val pet = result.value.body - println("Found: ${pet.name}") - } - is Either.Left -> when (val error = result.value) { - is HttpError.NotFound -> println("Pet not found") - is HttpError.Unauthorized -> println("Auth required") - is HttpError.Network -> println("Connection failed: ${error.cause}") - else -> println("Error ${error.statusCode}: ${error.body}") - } +val result: Either> = either { + client.getPet(petId = 123) } + +result.fold( + ifLeft = { error -> + when (error.type) { + HttpErrorType.Client -> println("Client error ${error.code}: ${error.message}") + HttpErrorType.Server -> println("Server error ${error.code}: ${error.message}") + HttpErrorType.Redirect -> println("Redirect ${error.code}") + HttpErrorType.Network -> println("Connection failed: ${error.message}") + } + }, + ifRight = { success -> + println("Found: ${success.body.name}") + } +) ``` -`HttpError` covers specific HTTP status codes as sealed subtypes: - -| Subtype | Status | -|------------------------|--------| -| `BadRequest` | 400 | -| `Unauthorized` | 401 | -| `Forbidden` | 403 | -| `NotFound` | 404 | -| `MethodNotAllowed` | 405 | -| `Conflict` | 409 | -| `Gone` | 410 | -| `UnprocessableEntity` | 422 | -| `TooManyRequests` | 429 | -| `InternalServerError` | 500 | -| `BadGateway` | 502 | -| `ServiceUnavailable` | 503 | -| `Network` | -- | -| `Other` | any | - -Network errors (connection timeouts, DNS failures) are caught and wrapped in `HttpError.Network` instead of propagating exceptions. +`HttpError` is a data class with the following fields: + +| Field | Type | Description | +|-----------|-----------------|------------------------------------------| +| `code` | `Int` | HTTP status code (or `0` for network errors) | +| `message` | `String` | Response body text or exception message | +| `type` | `HttpErrorType` | Category of the error | + +`HttpErrorType` categorizes errors: + +| `HttpErrorType` value | Covered statuses / scenario | +|-----------------------|-------------------------------------| +| `Client` | HTTP 4xx client errors | +| `Server` | HTTP 5xx server errors | +| `Redirect` | HTTP 3xx redirect responses | +| `Network` | I/O failures, timeouts, DNS issues | + +Network errors (connection timeouts, DNS failures) are caught and reported as `HttpError(code = 0, ..., type = HttpErrorType.Network)` instead of propagating exceptions. ### Serialization Setup @@ -202,7 +178,7 @@ Generated models use `@Serializable` from kotlinx.serialization. The client sets If your spec uses polymorphic types (`oneOf` / `anyOf` with discriminators), the generator produces a `SerializersModule` that is automatically registered with the internal JSON instance. No manual serialization configuration is needed. -If you need to customize the JSON configuration for external use (e.g., parsing API responses outside the client), use the same settings: +The internal `createHttpClient()` uses default `Json` settings (it does not set `ignoreUnknownKeys` or `isLenient`). If you need a `Json` instance for external use (e.g., parsing API responses outside the client), you may want to configure these yourself: ```kotlin val json = Json { From 1206d1ac35ad2aa124b92f7c0a25371612a2d0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 2 Apr 2026 18:32:08 +0200 Subject: [PATCH 3/6] docs: remove redundant sections and fix KDoc enum values Merge Authentication info into Creating the Client section to eliminate duplicate code example. Remove Multi-Spec Configuration section already covered in Usage. Fix ApiResponseGenerator KDoc listing 3 enum values instead of 4 (missing Redirect). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 41 ++----------------- .../core/gen/ApiResponseGenerator.kt | 2 +- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index bc6638f..1670820 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,9 @@ dependencies { ### Creating the Client Each generated client extends `ApiClientBase` and creates its own pre-configured `HttpClient` internally. -You only need to provide the base URL and authentication credentials: +You only need to provide the base URL and authentication credentials. -Generated client class names are derived from OpenAPI tags as `Api` (e.g., a `pets` tag produces `PetsApi`). If the spec has no tags, the class is named `DefaultApi`. +Class names are derived from OpenAPI tags as `Api` (e.g., a `pets` tag produces `PetsApi`). Untagged endpoints go to `DefaultApi`. ```kotlin val client = PetsApi( @@ -90,7 +90,7 @@ val client = PetsApi( ) ``` -Auth parameters are lambdas (`() -> String`), so you can supply a token provider that refreshes automatically: +The `token` parameter is a `() -> String` lambda called on every request and sent as a `Bearer` token in the `Authorization` header. This lets you supply a provider that refreshes automatically: ```kotlin val client = PetsApi( @@ -101,17 +101,6 @@ val client = PetsApi( The client implements `Closeable` -- call `client.close()` when done to release HTTP resources. -### Authentication - -The generated constructor always takes a `baseUrl: String` and a `token: () -> String` parameter. The `token` lambda is called on each request and its value is sent as a `Bearer` token in the `Authorization` header: - -```kotlin -val client = PetsApi( - baseUrl = "https://api.example.com", - token = { "your-bearer-token" }, -) -``` - ### Making Requests Every endpoint becomes a `suspend` function on the client. Functions use Arrow's [Raise](https://arrow-kt.io/docs/typed-errors/) for structured error handling -- they require a `context(Raise)` and return `HttpSuccess` on success: @@ -196,30 +185,6 @@ val json = Json { } ``` -### Multi-Spec Configuration - -When your project consumes multiple APIs, register each spec separately. -The plugin generates independent client classes per spec: - -```kotlin -justworks { - specs { - register("petstore") { - specFile = file("api/petstore.yaml") - packageName = "com.example.petstore" - } - register("payments") { - specFile = file("api/payments.yaml") - packageName = "com.example.payments" - apiPackage = "com.example.payments.client" - modelPackage = "com.example.payments.dto" - } - } -} -``` - -Each spec gets its own Gradle task (`justworksGenerate`) and output directory. The generated clients are independent and can be used side by side. - ## Publishing Releases are published to [Maven Central](https://central.sonatype.com/) automatically when a version tag (`v*`) is pushed. The CD pipeline runs CI checks first, then publishes signed artifacts via the [vanniktech maven-publish](https://github.com/vanniktech/gradle-maven-publish-plugin) plugin. diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiResponseGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiResponseGenerator.kt index f983258..19a6f40 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiResponseGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiResponseGenerator.kt @@ -11,7 +11,7 @@ import com.squareup.kotlinpoet.TypeVariableName /** * Generates [FileSpec]s containing: - * - `HttpErrorType` enum class with Client, Server, Network values + * - `HttpErrorType` enum class with Client, Server, Redirect, Network values * - `HttpError` data class with code, message, type fields * - `HttpSuccess` data class wrapping successful responses */ From 410fa8dfe001e97e42a4646a1bc5b580998e1ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 2 Apr 2026 18:38:15 +0200 Subject: [PATCH 4/6] docs: add supported features, code structure, and customization sections Add three new README sections: - Supported OpenAPI Features: type mapping table, composition/polymorphism support matrix, parameter and request body support - Generated Code Structure: output layout tree, model/api package contents, Gradle task reference - Customization: package overrides, Ktor engine selection, JSON configuration Remove redundant Serialization Setup section (now covered by Customization). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 175 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 151 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1670820..170202b 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,157 @@ Generated sources are automatically wired into Kotlin source sets, so `compileKo | `apiPackage` | No | `$packageName.api` | Package for API client classes | | `modelPackage` | No | `$packageName.model` | Package for model/data classes | +## Supported OpenAPI Features + +### Schema Types + +| OpenAPI type | Format | Kotlin type | +|----------------|--------------|-------------------| +| `string` | *(default)* | `String` | +| `string` | `date` | `LocalDate` | +| `string` | `date-time` | `Instant` | +| `string` | `uuid` | `Uuid` | +| `string` | `byte` | `ByteArray` | +| `string` | `binary` | `ByteArray` | +| `integer` | `int32` | `Int` | +| `integer` | `int64` | `Long` | +| `number` | `float` | `Float` | +| `number` | `double` | `Double` | +| `boolean` | -- | `Boolean` | +| `array` | -- | `List` | +| `object` | -- | data class | +| `additionalProperties` | -- | `Map` | + +Other string formats (`email`, `uri`, `hostname`, etc.) are kept as `String`. + +### Composition & Polymorphism + +| Feature | Support | Generated Kotlin | +|---------|---------|-----------------| +| `allOf` | Full | Merged data class (properties from all schemas) | +| `oneOf` with discriminator | Full | `sealed interface` + variant data classes with `@JsonClassDiscriminator` | +| `anyOf` with discriminator | Full | Same as `oneOf` | +| `anyOf` without discriminator | Partial | `sealed interface` + `JsonContentPolymorphicSerializer` (field-presence heuristic) | +| Discriminator mapping | Full | `@SerialName` on variants | + +A `SerializersModule` is auto-generated when discriminated polymorphic types are present. + +### Enums, Nullability, Defaults + +- **Enums** -- generated as `enum class` with `@SerialName` per constant. String and integer backing types supported. +- **Required properties** -- non-nullable constructor parameters. +- **Optional properties** -- nullable with `= null` default. +- **Default values** -- supported for primitives, dates, and enum references. +- **Inline schemas** -- auto-named from context (e.g. `CreatePetRequest`) and deduplicated by structure. + +### Parameters & Request Bodies + +| Feature | Status | +|---------|--------| +| Path parameters | Supported | +| Query parameters | Supported | +| Header parameters | Supported | +| Cookie parameters | Not yet supported | +| `application/json` request body | Supported | +| Form data / multipart | Not supported | + +### Not Supported + +Callbacks, links, webhooks, XML content types, and OpenAPI vendor extensions (`x-*`) are not processed. The plugin logs warnings for callbacks and links found in a spec. + +## Generated Code Structure + +The plugin produces two categories of output: **shared types** (generated once) and **per-spec types** (generated per registered spec). + +### Output Layout + +``` +build/generated/justworks/ +├── shared/kotlin/ +│ └── com/avsystem/justworks/ +│ ├── ApiClientBase.kt # Abstract base class + helper extensions +│ ├── HttpError.kt # HttpErrorType enum + HttpError data class +│ └── HttpSuccess.kt # HttpSuccess data class +│ +└── {specName}/ + └── com/example/ + ├── model/ + │ ├── Pet.kt # @Serializable data class + │ ├── PetStatus.kt # @Serializable enum class + │ ├── Shape.kt # sealed interface (oneOf/anyOf) + │ ├── Circle.kt # variant data class : Shape + │ ├── UuidSerializer.kt # (if spec uses UUID fields) + │ └── SerializersModule.kt # (if spec has polymorphic types) + └── api/ + └── PetsApi.kt # Client class per OpenAPI tag +``` + +### Model Package + +- **Data classes** -- one per named schema. Properties annotated with `@SerialName`, sorted required-first. +- **Enums** -- constants in `UPPER_SNAKE_CASE` with `@SerialName` for the wire value. +- **Sealed interfaces** -- for `oneOf`/`anyOf` schemas. Variants are separate data classes implementing the interface. +- **SerializersModule** -- top-level `val generatedSerializersModule` registering all polymorphic hierarchies. Only generated when needed. + +### API Package + +One client class per OpenAPI tag (e.g. `pets` tag -> `PetsApi`). Untagged endpoints go to `DefaultApi`. + +Each endpoint becomes a `suspend` function with `context(Raise)` that returns `HttpSuccess`. + +### Gradle Tasks + +| Task | Description | +|------|-------------| +| `justworksSharedTypes` | Generates shared types (once per build) | +| `justworksGenerate` | Generates code for one spec (depends on shared types) | +| `justworksGenerateAll` | Aggregate -- triggers all spec tasks | + +`compileKotlin` depends on `justworksGenerateAll`, so generation runs automatically. + +## Customization + +### Package Overrides + +Override the default `api` / `model` sub-packages per spec: + +```kotlin +justworks { + specs { + register("petstore") { + specFile = file("api/petstore.yaml") + packageName = "com.example.petstore" + apiPackage = "com.example.petstore.client" // default: packageName.api + modelPackage = "com.example.petstore.dto" // default: packageName.model + } + } +} +``` + +### Ktor Engine + +The generated `HttpClient` is created without an explicit engine, so Ktor uses whichever engine is on the classpath. Switch engines by changing your dependency: + +```kotlin +dependencies { + // Pick one: + implementation("io.ktor:ktor-client-cio:3.1.1") // CIO (default, pure Kotlin) + implementation("io.ktor:ktor-client-okhttp:3.1.1") // OkHttp + implementation("io.ktor:ktor-client-apache:3.1.1") // Apache +} +``` + +### JSON Configuration + +The internal `Json` instance uses default settings (`ignoreUnknownKeys = false`, `isLenient = false`). If you need a custom `Json` for use outside the client, configure it yourself and include the generated `SerializersModule` when your spec has polymorphic types: + +```kotlin +val json = Json { + ignoreUnknownKeys = true + serializersModule = com.example.petstore.model.generatedSerializersModule +} +``` + ## Generated Client Usage After running code generation, the plugin produces type-safe Kotlin client classes. @@ -161,30 +312,6 @@ result.fold( Network errors (connection timeouts, DNS failures) are caught and reported as `HttpError(code = 0, ..., type = HttpErrorType.Network)` instead of propagating exceptions. -### Serialization Setup - -Generated models use `@Serializable` from kotlinx.serialization. The client sets up JSON content negotiation internally via the `createHttpClient()` method. - -If your spec uses polymorphic types (`oneOf` / `anyOf` with discriminators), the generator produces a `SerializersModule` that is automatically registered with the internal JSON instance. No manual serialization configuration is needed. - -The internal `createHttpClient()` uses default `Json` settings (it does not set `ignoreUnknownKeys` or `isLenient`). If you need a `Json` instance for external use (e.g., parsing API responses outside the client), you may want to configure these yourself: - -```kotlin -val json = Json { - ignoreUnknownKeys = true // recommended: specs may evolve - isLenient = true // optional: tolerant parsing -} -``` - -For polymorphic types, register the generated `SerializersModule`: - -```kotlin -val json = Json { - ignoreUnknownKeys = true - serializersModule = com.example.petstore.model.generatedSerializersModule -} -``` - ## Publishing Releases are published to [Maven Central](https://central.sonatype.com/) automatically when a version tag (`v*`) is pushed. The CD pipeline runs CI checks first, then publishes signed artifacts via the [vanniktech maven-publish](https://github.com/vanniktech/gradle-maven-publish-plugin) plugin. From 700c06584c1b7fe376345ac91d5ceb547fd31ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 2 Apr 2026 18:39:37 +0200 Subject: [PATCH 5/6] style: reformat README tables and line wrapping Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 131 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 170202b..4585314 100644 --- a/README.md +++ b/README.md @@ -45,49 +45,50 @@ Each spec gets its own Gradle task (`justworksGenerate`) and output direct ./gradlew justworksGenerateAll ``` -Generated sources are automatically wired into Kotlin source sets, so `compileKotlin` depends on code generation -- no extra configuration needed. +Generated sources are automatically wired into Kotlin source sets, so `compileKotlin` depends on code generation -- no +extra configuration needed. ### Configuration options -| Property | Required | Default | Description | -|----------------|----------|--------------------------|--------------------------------------| -| `specFile` | Yes | -- | Path to the OpenAPI spec (.yaml/.json) | -| `packageName` | Yes | -- | Base package for generated code | -| `apiPackage` | No | `$packageName.api` | Package for API client classes | -| `modelPackage` | No | `$packageName.model` | Package for model/data classes | +| Property | Required | Default | Description | +|----------------|----------|----------------------|----------------------------------------| +| `specFile` | Yes | -- | Path to the OpenAPI spec (.yaml/.json) | +| `packageName` | Yes | -- | Base package for generated code | +| `apiPackage` | No | `$packageName.api` | Package for API client classes | +| `modelPackage` | No | `$packageName.model` | Package for model/data classes | ## Supported OpenAPI Features ### Schema Types -| OpenAPI type | Format | Kotlin type | -|----------------|--------------|-------------------| -| `string` | *(default)* | `String` | -| `string` | `date` | `LocalDate` | -| `string` | `date-time` | `Instant` | -| `string` | `uuid` | `Uuid` | -| `string` | `byte` | `ByteArray` | -| `string` | `binary` | `ByteArray` | -| `integer` | `int32` | `Int` | -| `integer` | `int64` | `Long` | -| `number` | `float` | `Float` | -| `number` | `double` | `Double` | -| `boolean` | -- | `Boolean` | -| `array` | -- | `List` | -| `object` | -- | data class | -| `additionalProperties` | -- | `Map` | +| OpenAPI type | Format | Kotlin type | +|------------------------|-------------|------------------| +| `string` | *(default)* | `String` | +| `string` | `date` | `LocalDate` | +| `string` | `date-time` | `Instant` | +| `string` | `uuid` | `Uuid` | +| `string` | `byte` | `ByteArray` | +| `string` | `binary` | `ByteArray` | +| `integer` | `int32` | `Int` | +| `integer` | `int64` | `Long` | +| `number` | `float` | `Float` | +| `number` | `double` | `Double` | +| `boolean` | -- | `Boolean` | +| `array` | -- | `List` | +| `object` | -- | data class | +| `additionalProperties` | -- | `Map` | Other string formats (`email`, `uri`, `hostname`, etc.) are kept as `String`. ### Composition & Polymorphism -| Feature | Support | Generated Kotlin | -|---------|---------|-----------------| -| `allOf` | Full | Merged data class (properties from all schemas) | -| `oneOf` with discriminator | Full | `sealed interface` + variant data classes with `@JsonClassDiscriminator` | -| `anyOf` with discriminator | Full | Same as `oneOf` | +| Feature | Support | Generated Kotlin | +|-------------------------------|---------|------------------------------------------------------------------------------------| +| `allOf` | Full | Merged data class (properties from all schemas) | +| `oneOf` with discriminator | Full | `sealed interface` + variant data classes with `@JsonClassDiscriminator` | +| `anyOf` with discriminator | Full | Same as `oneOf` | | `anyOf` without discriminator | Partial | `sealed interface` + `JsonContentPolymorphicSerializer` (field-presence heuristic) | -| Discriminator mapping | Full | `@SerialName` on variants | +| Discriminator mapping | Full | `@SerialName` on variants | A `SerializersModule` is auto-generated when discriminated polymorphic types are present. @@ -101,22 +102,24 @@ A `SerializersModule` is auto-generated when discriminated polymorphic types are ### Parameters & Request Bodies -| Feature | Status | -|---------|--------| -| Path parameters | Supported | -| Query parameters | Supported | -| Header parameters | Supported | -| Cookie parameters | Not yet supported | -| `application/json` request body | Supported | -| Form data / multipart | Not supported | +| Feature | Status | +|---------------------------------|-------------------| +| Path parameters | Supported | +| Query parameters | Supported | +| Header parameters | Supported | +| Cookie parameters | Not yet supported | +| `application/json` request body | Supported | +| Form data / multipart | Not supported | ### Not Supported -Callbacks, links, webhooks, XML content types, and OpenAPI vendor extensions (`x-*`) are not processed. The plugin logs warnings for callbacks and links found in a spec. +Callbacks, links, webhooks, XML content types, and OpenAPI vendor extensions (`x-*`) are not processed. The plugin logs +warnings for callbacks and links found in a spec. ## Generated Code Structure -The plugin produces two categories of output: **shared types** (generated once) and **per-spec types** (generated per registered spec). +The plugin produces two categories of output: **shared types** (generated once) and **per-spec types** (generated per +registered spec). ### Output Layout @@ -128,7 +131,7 @@ build/generated/justworks/ │ ├── HttpError.kt # HttpErrorType enum + HttpError data class │ └── HttpSuccess.kt # HttpSuccess data class │ -└── {specName}/ +└── specName/ └── com/example/ ├── model/ │ ├── Pet.kt # @Serializable data class @@ -146,7 +149,8 @@ build/generated/justworks/ - **Data classes** -- one per named schema. Properties annotated with `@SerialName`, sorted required-first. - **Enums** -- constants in `UPPER_SNAKE_CASE` with `@SerialName` for the wire value. - **Sealed interfaces** -- for `oneOf`/`anyOf` schemas. Variants are separate data classes implementing the interface. -- **SerializersModule** -- top-level `val generatedSerializersModule` registering all polymorphic hierarchies. Only generated when needed. +- **SerializersModule** -- top-level `val generatedSerializersModule` registering all polymorphic hierarchies. Only + generated when needed. ### API Package @@ -156,11 +160,11 @@ Each endpoint becomes a `suspend` function with `context(Raise)` that ### Gradle Tasks -| Task | Description | -|------|-------------| -| `justworksSharedTypes` | Generates shared types (once per build) | +| Task | Description | +|---------------------------|-------------------------------------------------------| +| `justworksSharedTypes` | Generates shared types (once per build) | | `justworksGenerate` | Generates code for one spec (depends on shared types) | -| `justworksGenerateAll` | Aggregate -- triggers all spec tasks | +| `justworksGenerateAll` | Aggregate -- triggers all spec tasks | `compileKotlin` depends on `justworksGenerateAll`, so generation runs automatically. @@ -185,7 +189,8 @@ justworks { ### Ktor Engine -The generated `HttpClient` is created without an explicit engine, so Ktor uses whichever engine is on the classpath. Switch engines by changing your dependency: +The generated `HttpClient` is created without an explicit engine, so Ktor uses whichever engine is on the classpath. +Switch engines by changing your dependency: ```kotlin dependencies { @@ -198,7 +203,9 @@ dependencies { ### JSON Configuration -The internal `Json` instance uses default settings (`ignoreUnknownKeys = false`, `isLenient = false`). If you need a custom `Json` for use outside the client, configure it yourself and include the generated `SerializersModule` when your spec has polymorphic types: +The internal `Json` instance uses default settings (`ignoreUnknownKeys = false`, `isLenient = false`). If you need a +custom `Json` for use outside the client, configure it yourself and include the generated `SerializersModule` when your +spec has polymorphic types: ```kotlin val json = Json { @@ -232,7 +239,8 @@ dependencies { Each generated client extends `ApiClientBase` and creates its own pre-configured `HttpClient` internally. You only need to provide the base URL and authentication credentials. -Class names are derived from OpenAPI tags as `Api` (e.g., a `pets` tag produces `PetsApi`). Untagged endpoints go to `DefaultApi`. +Class names are derived from OpenAPI tags as `Api` (e.g., a `pets` tag produces `PetsApi`). Untagged endpoints go +to `DefaultApi`. ```kotlin val client = PetsApi( @@ -241,7 +249,8 @@ val client = PetsApi( ) ``` -The `token` parameter is a `() -> String` lambda called on every request and sent as a `Bearer` token in the `Authorization` header. This lets you supply a provider that refreshes automatically: +The `token` parameter is a `() -> String` lambda called on every request and sent as a `Bearer` token in the +`Authorization` header. This lets you supply a provider that refreshes automatically: ```kotlin val client = PetsApi( @@ -254,7 +263,9 @@ The client implements `Closeable` -- call `client.close()` when done to release ### Making Requests -Every endpoint becomes a `suspend` function on the client. Functions use Arrow's [Raise](https://arrow-kt.io/docs/typed-errors/) for structured error handling -- they require a `context(Raise)` and return `HttpSuccess` on success: +Every endpoint becomes a `suspend` function on the client. Functions use +Arrow's [Raise](https://arrow-kt.io/docs/typed-errors/) for structured error handling -- they require a +`context(Raise)` and return `HttpSuccess` on success: ```kotlin // Inside a Raise context (e.g., within either { ... }) @@ -271,7 +282,8 @@ val result = client.findPets(status = "available", limit = 20) ### Error Handling -Generated endpoints use [Arrow's Raise](https://arrow-kt.io/docs/typed-errors/) -- errors are raised, not returned as `Either`. Use Arrow's `either { ... }` block to obtain an `Either>`: +Generated endpoints use [Arrow's Raise](https://arrow-kt.io/docs/typed-errors/) -- errors are raised, not returned as +`Either`. Use Arrow's `either { ... }` block to obtain an `Either>`: ```kotlin val result: Either> = either { @@ -295,26 +307,29 @@ result.fold( `HttpError` is a data class with the following fields: -| Field | Type | Description | -|-----------|-----------------|------------------------------------------| +| Field | Type | Description | +|-----------|-----------------|----------------------------------------------| | `code` | `Int` | HTTP status code (or `0` for network errors) | -| `message` | `String` | Response body text or exception message | -| `type` | `HttpErrorType` | Category of the error | +| `message` | `String` | Response body text or exception message | +| `type` | `HttpErrorType` | Category of the error | `HttpErrorType` categorizes errors: | `HttpErrorType` value | Covered statuses / scenario | -|-----------------------|-------------------------------------| +|-----------------------|------------------------------------| | `Client` | HTTP 4xx client errors | | `Server` | HTTP 5xx server errors | | `Redirect` | HTTP 3xx redirect responses | | `Network` | I/O failures, timeouts, DNS issues | -Network errors (connection timeouts, DNS failures) are caught and reported as `HttpError(code = 0, ..., type = HttpErrorType.Network)` instead of propagating exceptions. +Network errors (connection timeouts, DNS failures) are caught and reported as +`HttpError(code = 0, ..., type = HttpErrorType.Network)` instead of propagating exceptions. ## Publishing -Releases are published to [Maven Central](https://central.sonatype.com/) automatically when a version tag (`v*`) is pushed. The CD pipeline runs CI checks first, then publishes signed artifacts via the [vanniktech maven-publish](https://github.com/vanniktech/gradle-maven-publish-plugin) plugin. +Releases are published to [Maven Central](https://central.sonatype.com/) automatically when a version tag (`v*`) is +pushed. The CD pipeline runs CI checks first, then publishes signed artifacts via +the [vanniktech maven-publish](https://github.com/vanniktech/gradle-maven-publish-plugin) plugin. To trigger a release: From ff789f7a8d90c4a77f3eae6633d250b9c33a2f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 2 Apr 2026 18:48:29 +0200 Subject: [PATCH 6/6] docs: fix dependencies section -- add context-parameters flag and correct Arrow version Testing revealed two issues: - Generated code uses Kotlin context parameters, requiring -Xcontext-parameters compiler flag (was undocumented) - Arrow version 2.1.2 missing arrow.core.raise.context.raise, updated to 2.2.1.1 matching the actual project dependency Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4585314..4dad22e 100644 --- a/README.md +++ b/README.md @@ -221,16 +221,22 @@ Here is how to use them. ### Dependencies -Add the required runtime dependencies to your consuming project: +Add the required runtime dependencies and enable the experimental context parameters compiler flag: ```kotlin +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } +} + dependencies { implementation("io.ktor:ktor-client-core:3.1.1") implementation("io.ktor:ktor-client-cio:3.1.1") // or another engine (OkHttp, Apache, etc.) implementation("io.ktor:ktor-client-content-negotiation:3.1.1") implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") - implementation("io.arrow-kt:arrow-core:2.1.2") + implementation("io.arrow-kt:arrow-core:2.2.1.1") } ```