From 2196c044e8cf27f2535d2fbbf0e4b0df22a211a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 9 Apr 2026 20:36:30 +0200 Subject: [PATCH 1/3] feat(core): support HEAD, OPTIONS and TRACE HTTP methods (#35) Add HEAD, OPTIONS and TRACE to HttpMethod enum so the parser no longer silently drops operations using these methods. HEAD and OPTIONS use dedicated Ktor client functions; TRACE uses the generic request builder since Ktor has no trace() shorthand. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/avsystem/justworks/core/gen/Names.kt | 3 ++ .../core/gen/client/BodyGenerator.kt | 15 ++++++- .../avsystem/justworks/core/model/ApiSpec.kt | 5 ++- .../justworks/core/gen/ClientGeneratorTest.kt | 21 ++++++++++ .../justworks/core/parser/SpecParserTest.kt | 39 +++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt index 85ec282..63fdca4 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt @@ -24,6 +24,9 @@ val POST_FUN = MemberName("io.ktor.client.request", "post") val PUT_FUN = MemberName("io.ktor.client.request", "put") val DELETE_FUN = MemberName("io.ktor.client.request", "delete") val PATCH_FUN = MemberName("io.ktor.client.request", "patch") +val HEAD_FUN = MemberName("io.ktor.client.request", "head") +val OPTIONS_FUN = MemberName("io.ktor.client.request", "options") +val REQUEST_FUN = MemberName("io.ktor.client.request", "request") // ============================================================================ // Ktor Forms & Multipart diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt index af8bc2f..7b82555 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt @@ -10,14 +10,17 @@ import com.avsystem.justworks.core.gen.DELETE_FUN import com.avsystem.justworks.core.gen.ENCODE_PARAM_FUN import com.avsystem.justworks.core.gen.FORM_DATA_FUN import com.avsystem.justworks.core.gen.GET_FUN +import com.avsystem.justworks.core.gen.HEAD_FUN import com.avsystem.justworks.core.gen.HEADERS_CLASS import com.avsystem.justworks.core.gen.HEADERS_FUN import com.avsystem.justworks.core.gen.HTTP_HEADERS import com.avsystem.justworks.core.gen.HTTP_METHOD_CLASS +import com.avsystem.justworks.core.gen.OPTIONS_FUN import com.avsystem.justworks.core.gen.PARAMETERS_FUN import com.avsystem.justworks.core.gen.PATCH_FUN import com.avsystem.justworks.core.gen.POST_FUN import com.avsystem.justworks.core.gen.PUT_FUN +import com.avsystem.justworks.core.gen.REQUEST_FUN import com.avsystem.justworks.core.gen.SAFE_CALL import com.avsystem.justworks.core.gen.SET_BODY_FUN import com.avsystem.justworks.core.gen.SUBMIT_FORM_FUN @@ -70,9 +73,15 @@ internal object BodyGenerator { HttpMethod.PUT -> PUT_FUN HttpMethod.DELETE -> DELETE_FUN HttpMethod.PATCH -> PATCH_FUN + HttpMethod.HEAD -> HEAD_FUN + HttpMethod.OPTIONS -> OPTIONS_FUN + HttpMethod.TRACE -> REQUEST_FUN } beginControlFlow("$CLIENT.%M(%L)", httpMethodFun, urlString) + if (endpoint.method == HttpMethod.TRACE) { + addStatement("method = %T(%S)", HTTP_METHOD_CLASS, "TRACE") + } addCommonRequestParts(params) optionalGuard(endpoint.requestBody?.required ?: false, BODY) { @@ -184,7 +193,11 @@ internal object BodyGenerator { private fun CodeBlock.Builder.addHttpMethodIfNeeded(method: HttpMethod) { if (method != HttpMethod.POST) { - addStatement("method = %T.%L", HTTP_METHOD_CLASS, method.name.toPascalCase()) + if (method == HttpMethod.TRACE) { + addStatement("method = %T(%S)", HTTP_METHOD_CLASS, "TRACE") + } else { + addStatement("method = %T.%L", HTTP_METHOD_CLASS, method.name.toPascalCase()) + } } } diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt b/core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt index ed87933..53250ae 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt @@ -32,7 +32,10 @@ enum class HttpMethod { POST, PUT, DELETE, - PATCH + PATCH, + HEAD, + OPTIONS, + TRACE } data class Parameter( diff --git a/core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt b/core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt index 0ca1238..80a03f3 100644 --- a/core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt +++ b/core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt @@ -119,6 +119,9 @@ class ClientGeneratorTest { HttpMethod.PUT to "updatePet", HttpMethod.DELETE to "deletePet", HttpMethod.PATCH to "patchPet", + HttpMethod.HEAD to "headPet", + HttpMethod.OPTIONS to "optionsPet", + HttpMethod.TRACE to "tracePet", ) val endpoints = methods.map { (method, opId) -> endpoint(method = method, operationId = opId) }.toTypedArray() val cls = clientClass(*endpoints) @@ -144,6 +147,24 @@ class ClientGeneratorTest { funBodies["patchPet"]!!.contains("request.patch(") || funBodies["patchPet"]!!.contains("request.`patch`("), "PATCH method expected", ) + assertTrue( + funBodies["headPet"]!!.contains("request.head(") || funBodies["headPet"]!!.contains("request.`head`("), + "HEAD method expected", + ) + assertTrue( + funBodies["optionsPet"]!!.contains("request.options(") || + funBodies["optionsPet"]!!.contains("request.`options`("), + "OPTIONS method expected", + ) + assertTrue( + funBodies["tracePet"]!!.contains("request.request(") || + funBodies["tracePet"]!!.contains("request.`request`("), + "TRACE method expected (via request builder)", + ) + assertTrue( + funBodies["tracePet"]!!.contains("HttpMethod(\"TRACE\")"), + "TRACE should set explicit HttpMethod", + ) } // -- CLNT-04: Path parameters become function parameters -- diff --git a/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt b/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt index e531d94..45532ee 100644 --- a/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt +++ b/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt @@ -160,6 +160,45 @@ class SpecParserTest : SpecParserTestBase() { assertEquals("Pet", itemType.schemaName) } + // -- SPEC-01b: HEAD, OPTIONS, TRACE parsing -- + + @Test + fun `parse spec with HEAD, OPTIONS and TRACE methods`() { + val spec = parseSpec( + """ + openapi: 3.0.0 + info: + title: Test + version: 1.0.0 + paths: + /health: + head: + operationId: healthHead + tags: [Health] + responses: + '200': + description: OK + options: + operationId: healthOptions + tags: [Health] + responses: + '200': + description: OK + trace: + operationId: healthTrace + tags: [Health] + responses: + '200': + description: OK + """.trimIndent().toTempFile(), + ) + + assertEquals(3, spec.endpoints.size) + assertEquals(HttpMethod.HEAD, spec.endpoints.find { it.operationId == "healthHead" }?.method) + assertEquals(HttpMethod.OPTIONS, spec.endpoints.find { it.operationId == "healthOptions" }?.method) + assertEquals(HttpMethod.TRACE, spec.endpoints.find { it.operationId == "healthTrace" }?.method) + } + // -- SPEC-02: $ref resolution -- @Test From 8f99934d0377448a79dec6eda81bd747d768095d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 10 Apr 2026 09:43:17 +0200 Subject: [PATCH 2/3] fix: correct import ordering in BodyGenerator Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/avsystem/justworks/core/gen/client/BodyGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt index 7b82555..1d5bff2 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/BodyGenerator.kt @@ -10,9 +10,9 @@ import com.avsystem.justworks.core.gen.DELETE_FUN import com.avsystem.justworks.core.gen.ENCODE_PARAM_FUN import com.avsystem.justworks.core.gen.FORM_DATA_FUN import com.avsystem.justworks.core.gen.GET_FUN -import com.avsystem.justworks.core.gen.HEAD_FUN import com.avsystem.justworks.core.gen.HEADERS_CLASS import com.avsystem.justworks.core.gen.HEADERS_FUN +import com.avsystem.justworks.core.gen.HEAD_FUN import com.avsystem.justworks.core.gen.HTTP_HEADERS import com.avsystem.justworks.core.gen.HTTP_METHOD_CLASS import com.avsystem.justworks.core.gen.OPTIONS_FUN From 8a52371d9bcb69b650887606047e6d2b1f749937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 10 Apr 2026 09:47:28 +0200 Subject: [PATCH 3/3] fix(core): add override modifier to HttpError.message property Generated HttpError extends RuntimeException but its `message` property was missing the `override` modifier, causing compilation failures in downstream projects. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt index 202ea8c..02e5543 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt @@ -57,6 +57,7 @@ internal object ApiResponseGenerator { ).addProperty( PropertySpec .builder(MESSAGE, STRING) + .addModifiers(KModifier.OVERRIDE) .initializer(MESSAGE) .build(), ).addProperty(