Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ fun String.toEnumConstantName(): String {
*/
fun String.toInlinedName(): String = replace(".", "_")

/**
* Sanitizes a string for safe inclusion in KDoc.
* Escapes comment terminators that would break generated Kotlin source.
*/
fun String.sanitizeKdoc(): String = replace("*/", "*/")
.replace("/*", "/*")

/**
* Generates a PascalCase operation name from HTTP method and path.
* Path parameters like {id} become "ById", {userId} becomes "ByUserId".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.avsystem.justworks.core.gen.client.BodyGenerator.buildFunctionBody
import com.avsystem.justworks.core.gen.client.ParametersGenerator.buildBodyParams
import com.avsystem.justworks.core.gen.client.ParametersGenerator.buildNullableParameter
import com.avsystem.justworks.core.gen.invoke
import com.avsystem.justworks.core.gen.sanitizeKdoc
import com.avsystem.justworks.core.gen.toCamelCase
import com.avsystem.justworks.core.gen.toPascalCase
import com.avsystem.justworks.core.gen.toTypeName
Expand Down Expand Up @@ -141,6 +142,25 @@ internal object ClientGenerator {
funBuilder.addParameters(buildBodyParams(endpoint.requestBody))
}

val kdocParts = mutableListOf<String>()
endpoint.summary?.let { kdocParts.add(it.sanitizeKdoc()) }
endpoint.description?.let {
if (kdocParts.isNotEmpty()) kdocParts.add("")
kdocParts.add(it.sanitizeKdoc())
}
val paramDocs = endpoint.parameters.filter { it.description != null }
if (paramDocs.isNotEmpty() && kdocParts.isNotEmpty()) kdocParts.add("")
paramDocs.forEach { param ->
kdocParts.add("@param ${param.name.toCamelCase()} ${param.description?.sanitizeKdoc()}")
}
if (kdocParts.isNotEmpty()) {
funBuilder.addKdoc("%L", kdocParts.joinToString("\n"))
}
if (returnBodyType != UNIT) {
if (kdocParts.isNotEmpty()) funBuilder.addKdoc("\n\n")
funBuilder.addKdoc("@return [%T] containing [%T] on success", HTTP_SUCCESS, returnBodyType)
}

funBuilder.addCode(buildFunctionBody(endpoint, params, returnBodyType))

return funBuilder.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.avsystem.justworks.core.gen.UUID_TYPE
import com.avsystem.justworks.core.gen.invoke
import com.avsystem.justworks.core.gen.resolveInlineTypes
import com.avsystem.justworks.core.gen.resolveTypeRef
import com.avsystem.justworks.core.gen.sanitizeKdoc
import com.avsystem.justworks.core.gen.shared.SerializersModuleGenerator
import com.avsystem.justworks.core.gen.toCamelCase
import com.avsystem.justworks.core.gen.toEnumConstantName
Expand Down Expand Up @@ -244,7 +245,7 @@ internal object ModelGenerator {
}

if (schema.description != null) {
typeSpec.addKdoc("%L", schema.description)
typeSpec.addKdoc("%L", schema.description.sanitizeKdoc())
}

val fileBuilder = FileSpec.builder(className).addType(typeSpec.build())
Expand Down Expand Up @@ -401,7 +402,7 @@ internal object ModelGenerator {
.builder(SERIAL_NAME)
.addMember("%S", prop.name)
.build(),
)
).apply { prop.description?.let { addKdoc("%L", it.sanitizeKdoc()) } }

propBuilder.build()
}
Expand All @@ -424,7 +425,7 @@ internal object ModelGenerator {
}

if (schema.description != null) {
typeSpec.addKdoc("%L", schema.description)
typeSpec.addKdoc("%L", schema.description.sanitizeKdoc())
}

val fileBuilder = FileSpec.builder(className).addType(typeSpec.build())
Expand Down Expand Up @@ -524,14 +525,15 @@ internal object ModelGenerator {
.addAnnotation(
AnnotationSpec
.builder(SERIAL_NAME)
.addMember("%S", value)
.addMember("%S", value.name)
.build(),
).build()
typeSpec.addEnumConstant(enumRegistry.register(value.toEnumConstantName()), anonymousClass)
).apply { value.description?.let { addKdoc("%L", it.sanitizeKdoc()) } }
.build()
typeSpec.addEnumConstant(enumRegistry.register(value.name.toEnumConstantName()), anonymousClass)
}

if (enum.description != null) {
typeSpec.addKdoc("%L", enum.description)
typeSpec.addKdoc("%L", enum.description.sanitizeKdoc())
}

return FileSpec
Expand Down Expand Up @@ -642,7 +644,7 @@ internal object ModelGenerator {
val typeAlias = TypeAliasSpec.builder(schema.name, primitiveType)

if (schema.description != null) {
typeAlias.addKdoc("%L", schema.description)
typeAlias.addKdoc("%L", schema.description.sanitizeKdoc())
}

return FileSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class Endpoint(
val method: HttpMethod,
val operationId: String,
val summary: String?,
val description: String?,
val tags: List<String>,
val parameters: List<Parameter>,
val requestBody: RequestBody?,
Expand Down Expand Up @@ -94,8 +95,10 @@ data class EnumModel(
val name: String,
val description: String?,
val type: EnumBackingType,
val values: List<String>,
)
val values: List<Value>,
) {
data class Value(val name: String, val description: String? = null)
}

enum class EnumBackingType {
STRING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.parser.core.models.ParseOptions
import java.io.File
import java.util.IdentityHashMap
import kotlin.collections.emptyMap
import io.swagger.v3.oas.models.parameters.Parameter as SwaggerParameter

/**
Expand Down Expand Up @@ -220,6 +221,7 @@ object SpecParser {
method = method,
operationId = operationId,
summary = operation.summary,
description = operation.description,
tags = operation.tags.orEmpty(),
parameters = mergedParams,
requestBody = requestBody,
Expand Down Expand Up @@ -291,12 +293,21 @@ object SpecParser {
)
}

private fun extractEnumModel(name: String, schema: Schema<*>): EnumModel = EnumModel(
name = name,
description = schema.description,
type = schema.type.toEnumOrNull<EnumBackingType>() ?: EnumBackingType.STRING,
values = schema.enum.map { it.toString() },
)
private fun extractEnumModel(name: String, schema: Schema<*>): EnumModel {
val enumValues = schema.enum.map { it.toString() }
val valueDescriptions = when (val ext = schema.extensions?.get("x-enum-descriptions")) {
is List<*> if ext.size == enumValues.size -> enumValues.zip(ext).toMap()
is Map<*, *> -> ext
else -> emptyMap()
}.mapNotNull { (k, v) -> if (k is String && v is String) k to v else null }.toMap()

return EnumModel(
name = name,
description = schema.description,
type = schema.type.toEnumOrNull<EnumBackingType>() ?: EnumBackingType.STRING,
values = enumValues.map { EnumModel.Value(it, valueDescriptions[it]) },
)
}

// --- allOf property merging ---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ClientGeneratorTest {
private val apiPackage = "com.example.api"
private val modelPackage = "com.example.model"

private fun generate(spec: ApiSpec, hasPolymorphicTypes: Boolean = false,): List<FileSpec> =
private fun generate(spec: ApiSpec, hasPolymorphicTypes: Boolean = false): List<FileSpec> =
context(ModelPackage(modelPackage), ApiPackage(apiPackage)) {
ClientGenerator.generate(spec, hasPolymorphicTypes, NameRegistry())
}
Expand All @@ -44,6 +44,8 @@ class ClientGeneratorTest {
path: String = "/pets",
method: HttpMethod = HttpMethod.GET,
operationId: String = "listPets",
summary: String? = null,
description: String? = null,
tags: List<String> = listOf("Pets"),
parameters: List<Parameter> = emptyList(),
requestBody: RequestBody? = null,
Expand All @@ -55,7 +57,8 @@ class ClientGeneratorTest {
path = path,
method = method,
operationId = operationId,
summary = null,
summary = summary,
description = description,
tags = tags,
parameters = parameters,
requestBody = requestBody,
Expand Down Expand Up @@ -646,4 +649,74 @@ class ClientGeneratorTest {
val body = funSpec.body.toString()
assertTrue(body.contains("toEmptyResult"), "Expected toEmptyResult call")
}

// -- DOCS-03: Endpoint KDoc generation --

@Test
fun `endpoint with summary generates KDoc`() {
val ep = endpoint(summary = "List all pets")
val cls = clientClass(ep)
val funSpec = cls.funSpecs.first { it.name == "listPets" }
assertTrue(
funSpec.kdoc.toString().contains("List all pets"),
"Expected KDoc with summary, got: ${funSpec.kdoc}",
)
}

@Test
fun `endpoint with summary and description generates KDoc with both`() {
val ep = endpoint(summary = "List pets", description = "Returns a paginated list of pets")
val cls = clientClass(ep)
val funSpec = cls.funSpecs.first { it.name == "listPets" }
val kdoc = funSpec.kdoc.toString()
assertTrue(kdoc.contains("List pets"), "Expected summary in KDoc, got: $kdoc")
assertTrue(kdoc.contains("Returns a paginated list of pets"), "Expected description in KDoc, got: $kdoc")
}

@Test
fun `endpoint with parameter descriptions generates param KDoc`() {
val ep = endpoint(
parameters = listOf(
Parameter(
"limit",
ParameterLocation.QUERY,
false,
TypeRef.Primitive(PrimitiveType.INT),
"Max items to return",
),
),
)
val cls = clientClass(ep)
val funSpec = cls.funSpecs.first { it.name == "listPets" }
val kdoc = funSpec.kdoc.toString()
assertTrue(kdoc.contains("@param"), "Expected @param in KDoc, got: $kdoc")
assertTrue(kdoc.contains("Max items to return"), "Expected param description in KDoc, got: $kdoc")
}

@Test
fun `endpoint with non-Unit return generates return KDoc`() {
val ep = endpoint(
responses = mapOf("200" to Response("200", "OK", TypeRef.Reference("Pet"))),
)
val cls = clientClass(ep)
val funSpec = cls.funSpecs.first { it.name == "listPets" }
val kdoc = funSpec.kdoc.toString()
assertTrue(kdoc.contains("@return"), "Expected @return in KDoc, got: $kdoc")
}

@Test
fun `endpoint without descriptions generates no KDoc`() {
val ep = endpoint(
summary = null,
description = null,
parameters = emptyList(),
responses = mapOf("204" to Response("204", "No content", null)),
)
val cls = clientClass(ep)
val funSpec = cls.funSpecs.first { it.name == "listPets" }
assertTrue(
funSpec.kdoc.toString().isEmpty(),
"Expected no KDoc when no descriptions, got: ${funSpec.kdoc}",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class InlineTypeResolverTest {
parameters = emptyList(),
requestBody = null,
responses = mapOf("200" to Response("200", null, inline)),
description = null,
),
),
)
Expand Down Expand Up @@ -157,6 +158,7 @@ class InlineTypeResolverTest {
parameters = emptyList(),
requestBody = RequestBody(true, ContentType.JSON_CONTENT_TYPE, inline),
responses = emptyMap(),
description = null,
),
),
)
Expand Down
Loading
Loading