Skip to content

Commit b6a4077

Browse files
authored
Merge pull request #33 from AVSystem/docs/usage-guide
2 parents 40f2baf + 2ed5760 commit b6a4077

2 files changed

Lines changed: 286 additions & 9 deletions

File tree

README.md

Lines changed: 285 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,297 @@ Each spec gets its own Gradle task (`justworksGenerate<Name>`) and output direct
4545
./gradlew justworksGenerateAll
4646
```
4747

48-
Generated sources are automatically wired into Kotlin source sets, so `compileKotlin` depends on code generation -- no extra configuration needed.
48+
Generated sources are automatically wired into Kotlin source sets, so `compileKotlin` depends on code generation -- no
49+
extra configuration needed.
4950

5051
### Configuration options
5152

52-
| Property | Required | Default | Description |
53-
|----------------|----------|--------------------------|--------------------------------------|
54-
| `specFile` | Yes | -- | Path to the OpenAPI spec (.yaml/.json) |
55-
| `packageName` | Yes | -- | Base package for generated code |
56-
| `apiPackage` | No | `$packageName.api` | Package for API client classes |
57-
| `modelPackage` | No | `$packageName.model` | Package for model/data classes |
53+
| Property | Required | Default | Description |
54+
|----------------|----------|----------------------|----------------------------------------|
55+
| `specFile` | Yes | -- | Path to the OpenAPI spec (.yaml/.json) |
56+
| `packageName` | Yes | -- | Base package for generated code |
57+
| `apiPackage` | No | `$packageName.api` | Package for API client classes |
58+
| `modelPackage` | No | `$packageName.model` | Package for model/data classes |
59+
60+
## Supported OpenAPI Features
61+
62+
### Schema Types
63+
64+
| OpenAPI type | Format | Kotlin type |
65+
|------------------------|-------------|------------------|
66+
| `string` | *(default)* | `String` |
67+
| `string` | `date` | `LocalDate` |
68+
| `string` | `date-time` | `Instant` |
69+
| `string` | `uuid` | `Uuid` |
70+
| `string` | `byte` | `ByteArray` |
71+
| `string` | `binary` | `ByteArray` |
72+
| `integer` | `int32` | `Int` |
73+
| `integer` | `int64` | `Long` |
74+
| `number` | `float` | `Float` |
75+
| `number` | `double` | `Double` |
76+
| `boolean` | -- | `Boolean` |
77+
| `array` | -- | `List<T>` |
78+
| `object` | -- | data class |
79+
| `additionalProperties` | -- | `Map<String, T>` |
80+
81+
Other string formats (`email`, `uri`, `hostname`, etc.) are kept as `String`.
82+
83+
### Composition & Polymorphism
84+
85+
| Feature | Support | Generated Kotlin |
86+
|-------------------------------|---------|------------------------------------------------------------------------------------|
87+
| `allOf` | Full | Merged data class (properties from all schemas) |
88+
| `oneOf` with discriminator | Full | `sealed interface` + variant data classes with `@JsonClassDiscriminator` |
89+
| `anyOf` with discriminator | Full | Same as `oneOf` |
90+
| `anyOf` without discriminator | Partial | `sealed interface` + `JsonContentPolymorphicSerializer` (field-presence heuristic) |
91+
| Discriminator mapping | Full | `@SerialName` on variants |
92+
93+
A `SerializersModule` is auto-generated when discriminated polymorphic types are present.
94+
95+
### Enums, Nullability, Defaults
96+
97+
- **Enums** -- generated as `enum class` with `@SerialName` per constant. String and integer backing types supported.
98+
- **Required properties** -- non-nullable constructor parameters.
99+
- **Optional properties** -- nullable with `= null` default.
100+
- **Default values** -- supported for primitives, dates, and enum references.
101+
- **Inline schemas** -- auto-named from context (e.g. `CreatePetRequest`) and deduplicated by structure.
102+
103+
### Parameters & Request Bodies
104+
105+
| Feature | Status |
106+
|---------------------------------|-------------------|
107+
| Path parameters | Supported |
108+
| Query parameters | Supported |
109+
| Header parameters | Supported |
110+
| Cookie parameters | Not yet supported |
111+
| `application/json` request body | Supported |
112+
| Form data / multipart | Not supported |
113+
114+
### Not Supported
115+
116+
Callbacks, links, webhooks, XML content types, and OpenAPI vendor extensions (`x-*`) are not processed. The plugin logs
117+
warnings for callbacks and links found in a spec.
118+
119+
## Generated Code Structure
120+
121+
The plugin produces two categories of output: **shared types** (generated once) and **per-spec types** (generated per
122+
registered spec).
123+
124+
### Output Layout
125+
126+
```
127+
build/generated/justworks/
128+
├── shared/kotlin/
129+
│ └── com/avsystem/justworks/
130+
│ ├── ApiClientBase.kt # Abstract base class + helper extensions
131+
│ ├── HttpError.kt # HttpErrorType enum + HttpError data class
132+
│ └── HttpSuccess.kt # HttpSuccess<T> data class
133+
134+
└── specName/
135+
└── com/example/
136+
├── model/
137+
│ ├── Pet.kt # @Serializable data class
138+
│ ├── PetStatus.kt # @Serializable enum class
139+
│ ├── Shape.kt # sealed interface (oneOf/anyOf)
140+
│ ├── Circle.kt # variant data class : Shape
141+
│ ├── UuidSerializer.kt # (if spec uses UUID fields)
142+
│ └── SerializersModule.kt # (if spec has polymorphic types)
143+
└── api/
144+
└── PetsApi.kt # Client class per OpenAPI tag
145+
```
146+
147+
### Model Package
148+
149+
- **Data classes** -- one per named schema. Properties annotated with `@SerialName`, sorted required-first.
150+
- **Enums** -- constants in `UPPER_SNAKE_CASE` with `@SerialName` for the wire value.
151+
- **Sealed interfaces** -- for `oneOf`/`anyOf` schemas. Variants are separate data classes implementing the interface.
152+
- **SerializersModule** -- top-level `val generatedSerializersModule` registering all polymorphic hierarchies. Only
153+
generated when needed.
154+
155+
### API Package
156+
157+
One client class per OpenAPI tag (e.g. `pets` tag -> `PetsApi`). Untagged endpoints go to `DefaultApi`.
158+
159+
Each endpoint becomes a `suspend` function with `context(Raise<HttpError>)` that returns `HttpSuccess<T>`.
160+
161+
### Gradle Tasks
162+
163+
| Task | Description |
164+
|---------------------------|-------------------------------------------------------|
165+
| `justworksSharedTypes` | Generates shared types (once per build) |
166+
| `justworksGenerate<Name>` | Generates code for one spec (depends on shared types) |
167+
| `justworksGenerateAll` | Aggregate -- triggers all spec tasks |
168+
169+
`compileKotlin` depends on `justworksGenerateAll`, so generation runs automatically.
170+
171+
## Customization
172+
173+
### Package Overrides
174+
175+
Override the default `api` / `model` sub-packages per spec:
176+
177+
```kotlin
178+
justworks {
179+
specs {
180+
register("petstore") {
181+
specFile = file("api/petstore.yaml")
182+
packageName = "com.example.petstore"
183+
apiPackage = "com.example.petstore.client" // default: packageName.api
184+
modelPackage = "com.example.petstore.dto" // default: packageName.model
185+
}
186+
}
187+
}
188+
```
189+
190+
### Ktor Engine
191+
192+
The generated `HttpClient` is created without an explicit engine, so Ktor uses whichever engine is on the classpath.
193+
Switch engines by changing your dependency:
194+
195+
```kotlin
196+
dependencies {
197+
// Pick one:
198+
implementation("io.ktor:ktor-client-cio:3.1.1") // CIO (default, pure Kotlin)
199+
implementation("io.ktor:ktor-client-okhttp:3.1.1") // OkHttp
200+
implementation("io.ktor:ktor-client-apache:3.1.1") // Apache
201+
}
202+
```
203+
204+
### JSON Configuration
205+
206+
The internal `Json` instance uses default settings (`ignoreUnknownKeys = false`, `isLenient = false`). If you need a
207+
custom `Json` for use outside the client, configure it yourself and include the generated `SerializersModule` when your
208+
spec has polymorphic types:
209+
210+
```kotlin
211+
val json = Json {
212+
ignoreUnknownKeys = true
213+
serializersModule = com.example.petstore.model.generatedSerializersModule
214+
}
215+
```
216+
217+
## Generated Client Usage
218+
219+
After running code generation, the plugin produces type-safe Kotlin client classes.
220+
Here is how to use them.
221+
222+
### Dependencies
223+
224+
Add the required runtime dependencies and enable the experimental context parameters compiler flag:
225+
226+
```kotlin
227+
kotlin {
228+
compilerOptions {
229+
freeCompilerArgs.add("-Xcontext-parameters")
230+
}
231+
}
232+
233+
dependencies {
234+
implementation("io.ktor:ktor-client-core:3.1.1")
235+
implementation("io.ktor:ktor-client-cio:3.1.1") // or another engine (OkHttp, Apache, etc.)
236+
implementation("io.ktor:ktor-client-content-negotiation:3.1.1")
237+
implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.1")
238+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
239+
implementation("io.arrow-kt:arrow-core:2.2.1.1")
240+
}
241+
```
242+
243+
### Creating the Client
244+
245+
Each generated client extends `ApiClientBase` and creates its own pre-configured `HttpClient` internally.
246+
You only need to provide the base URL and authentication credentials.
247+
248+
Class names are derived from OpenAPI tags as `<Tag>Api` (e.g., a `pets` tag produces `PetsApi`). Untagged endpoints go
249+
to `DefaultApi`.
250+
251+
```kotlin
252+
val client = PetsApi(
253+
baseUrl = "https://api.example.com",
254+
token = { "your-bearer-token" },
255+
)
256+
```
257+
258+
The `token` parameter is a `() -> String` lambda called on every request and sent as a `Bearer` token in the
259+
`Authorization` header. This lets you supply a provider that refreshes automatically:
260+
261+
```kotlin
262+
val client = PetsApi(
263+
baseUrl = "https://api.example.com",
264+
token = { tokenStore.getAccessToken() },
265+
)
266+
```
267+
268+
The client implements `Closeable` -- call `client.close()` when done to release HTTP resources.
269+
270+
### Making Requests
271+
272+
Every endpoint becomes a `suspend` function on the client. Functions use
273+
Arrow's [Raise](https://arrow-kt.io/docs/typed-errors/) for structured error handling -- they require a
274+
`context(Raise<HttpError>)` and return `HttpSuccess<T>` on success:
275+
276+
```kotlin
277+
// Inside a Raise<HttpError> context (e.g., within either { ... })
278+
val result: HttpSuccess<List<Pet>> = client.listPets(limit = 10)
279+
println(result.body) // the deserialized response body
280+
println(result.code) // the HTTP status code
281+
```
282+
283+
Path, query, and header parameters map to function arguments. Optional parameters default to `null`:
284+
285+
```kotlin
286+
val result = client.findPets(status = "available", limit = 20)
287+
```
288+
289+
### Error Handling
290+
291+
Generated endpoints use [Arrow's Raise](https://arrow-kt.io/docs/typed-errors/) -- errors are raised, not returned as
292+
`Either`. Use Arrow's `either { ... }` block to obtain an `Either<HttpError, HttpSuccess<T>>`:
293+
294+
```kotlin
295+
val result: Either<HttpError, HttpSuccess<Pet>> = either {
296+
client.getPet(petId = 123)
297+
}
298+
299+
result.fold(
300+
ifLeft = { error ->
301+
when (error.type) {
302+
HttpErrorType.Client -> println("Client error ${error.code}: ${error.message}")
303+
HttpErrorType.Server -> println("Server error ${error.code}: ${error.message}")
304+
HttpErrorType.Redirect -> println("Redirect ${error.code}")
305+
HttpErrorType.Network -> println("Connection failed: ${error.message}")
306+
}
307+
},
308+
ifRight = { success ->
309+
println("Found: ${success.body.name}")
310+
}
311+
)
312+
```
313+
314+
`HttpError` is a data class with the following fields:
315+
316+
| Field | Type | Description |
317+
|-----------|-----------------|----------------------------------------------|
318+
| `code` | `Int` | HTTP status code (or `0` for network errors) |
319+
| `message` | `String` | Response body text or exception message |
320+
| `type` | `HttpErrorType` | Category of the error |
321+
322+
`HttpErrorType` categorizes errors:
323+
324+
| `HttpErrorType` value | Covered statuses / scenario |
325+
|-----------------------|------------------------------------|
326+
| `Client` | HTTP 4xx client errors |
327+
| `Server` | HTTP 5xx server errors |
328+
| `Redirect` | HTTP 3xx redirect responses |
329+
| `Network` | I/O failures, timeouts, DNS issues |
330+
331+
Network errors (connection timeouts, DNS failures) are caught and reported as
332+
`HttpError(code = 0, ..., type = HttpErrorType.Network)` instead of propagating exceptions.
58333

59334
## Publishing
60335

61-
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.
336+
Releases are published to [Maven Central](https://central.sonatype.com/) automatically when a version tag (`v*`) is
337+
pushed. The CD pipeline runs CI checks first, then publishes signed artifacts via
338+
the [vanniktech maven-publish](https://github.com/vanniktech/gradle-maven-publish-plugin) plugin.
62339

63340
To trigger a release:
64341

core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import com.squareup.kotlinpoet.TypeVariableName
1616

1717
/**
1818
* Generates [com.squareup.kotlinpoet.FileSpec]s containing:
19-
* - `HttpErrorType` enum class with Client, Server, Network values
19+
* - `HttpErrorType` enum class with Client, Server, Redirect, Network values
2020
* - `HttpError` data class with code, message, type fields
2121
* - `HttpSuccess<T>` data class wrapping successful responses
2222
*/

0 commit comments

Comments
 (0)