diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt index 5599627..af4105b 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt @@ -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". diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/ClientGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/ClientGenerator.kt index 55a0cf2..30e18a2 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/ClientGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/client/ClientGenerator.kt @@ -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 @@ -141,6 +142,25 @@ internal object ClientGenerator { funBuilder.addParameters(buildBodyParams(endpoint.requestBody)) } + val kdocParts = mutableListOf() + 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() diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/model/ModelGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/model/ModelGenerator.kt index 60e4061..4281efe 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/model/ModelGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/model/ModelGenerator.kt @@ -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 @@ -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()) @@ -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() } @@ -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()) @@ -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 @@ -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 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 8c4f6c1..ed87933 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 @@ -20,6 +20,7 @@ data class Endpoint( val method: HttpMethod, val operationId: String, val summary: String?, + val description: String?, val tags: List, val parameters: List, val requestBody: RequestBody?, @@ -94,8 +95,10 @@ data class EnumModel( val name: String, val description: String?, val type: EnumBackingType, - val values: List, -) + val values: List, +) { + data class Value(val name: String, val description: String? = null) +} enum class EnumBackingType { STRING, diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt b/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt index 97e0f9c..4ae1c88 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt @@ -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 /** @@ -220,6 +221,7 @@ object SpecParser { method = method, operationId = operationId, summary = operation.summary, + description = operation.description, tags = operation.tags.orEmpty(), parameters = mergedParams, requestBody = requestBody, @@ -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.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.STRING, + values = enumValues.map { EnumModel.Value(it, valueDescriptions[it]) }, + ) + } // --- allOf property merging --- 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 4f7a231..6e353b2 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 @@ -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 = + private fun generate(spec: ApiSpec, hasPolymorphicTypes: Boolean = false): List = context(ModelPackage(modelPackage), ApiPackage(apiPackage)) { ClientGenerator.generate(spec, hasPolymorphicTypes, NameRegistry()) } @@ -44,6 +44,8 @@ class ClientGeneratorTest { path: String = "/pets", method: HttpMethod = HttpMethod.GET, operationId: String = "listPets", + summary: String? = null, + description: String? = null, tags: List = listOf("Pets"), parameters: List = emptyList(), requestBody: RequestBody? = null, @@ -55,7 +57,8 @@ class ClientGeneratorTest { path = path, method = method, operationId = operationId, - summary = null, + summary = summary, + description = description, tags = tags, parameters = parameters, requestBody = requestBody, @@ -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}", + ) + } } diff --git a/core/src/test/kotlin/com/avsystem/justworks/core/gen/InlineTypeResolverTest.kt b/core/src/test/kotlin/com/avsystem/justworks/core/gen/InlineTypeResolverTest.kt index 92f0ca8..7c72fd1 100644 --- a/core/src/test/kotlin/com/avsystem/justworks/core/gen/InlineTypeResolverTest.kt +++ b/core/src/test/kotlin/com/avsystem/justworks/core/gen/InlineTypeResolverTest.kt @@ -124,6 +124,7 @@ class InlineTypeResolverTest { parameters = emptyList(), requestBody = null, responses = mapOf("200" to Response("200", null, inline)), + description = null, ), ), ) @@ -157,6 +158,7 @@ class InlineTypeResolverTest { parameters = emptyList(), requestBody = RequestBody(true, ContentType.JSON_CONTENT_TYPE, inline), responses = emptyMap(), + description = null, ), ), ) diff --git a/core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorTest.kt b/core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorTest.kt index 638d437..417d3d1 100644 --- a/core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorTest.kt +++ b/core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorTest.kt @@ -186,7 +186,7 @@ class ModelGeneratorTest { name = "PetStatus", description = null, type = EnumBackingType.STRING, - values = listOf("available", "pending", "sold"), + values = listOf(EnumModel.Value("available"), EnumModel.Value("pending"), EnumModel.Value("sold")), ) @Test @@ -247,7 +247,7 @@ class ModelGeneratorTest { name = "Priority", description = null, type = EnumBackingType.INTEGER, - values = listOf("1", "2", "3"), + values = listOf(EnumModel.Value("1"), EnumModel.Value("2"), EnumModel.Value("3")), ) val files = generate(spec(enums = listOf(intEnum))) val typeSpec = files[0].members.filterIsInstance()[0] @@ -456,7 +456,13 @@ class ModelGeneratorTest { description = null, properties = listOf( - PropertyModel("active", TypeRef.Primitive(PrimitiveType.BOOLEAN), null, false, true), + PropertyModel( + "active", + TypeRef.Primitive(PrimitiveType.BOOLEAN), + null, + nullable = false, + defaultValue = true, + ), ), requiredProperties = setOf("active"), allOf = null, @@ -592,7 +598,7 @@ class ModelGeneratorTest { name = "Status", description = null, type = EnumBackingType.STRING, - values = listOf("active", "pending", "closed"), + values = listOf(EnumModel.Value("active"), EnumModel.Value("pending"), EnumModel.Value("closed")), ) val schema = SchemaModel( @@ -823,7 +829,7 @@ class ModelGeneratorTest { @Test fun `property named 'class' generates backtick-escaped name with SerialName`() { - // KotlinPoet auto-escapes hard keywords — 'class' should become `class` in generated source + // KotlinPoet auto-escapes hard keywords — 'class' should become `class` in a generated source val schema = SchemaModel( name = "Reserved", description = null, @@ -849,7 +855,7 @@ class ModelGeneratorTest { @Test fun `property named 'object' generates backtick-escaped name with SerialName`() { - // KotlinPoet auto-escapes hard keywords — 'object' should become `object` in generated source + // KotlinPoet auto-escapes hard keywords — 'object' should become `object` in a generated source val schema = SchemaModel( name = "Item", description = null, @@ -1129,7 +1135,7 @@ class ModelGeneratorTest { val typeSpec = files .first() .members - .filterIsInstance() + .filterIsInstance() .first() val constructor = assertNotNull(typeSpec.primaryConstructor) val param = constructor.parameters.first { it.name == "response" } @@ -1154,7 +1160,7 @@ class ModelGeneratorTest { val typeSpec = files .first() .members - .filterIsInstance() + .filterIsInstance() .first() val constructor = assertNotNull(typeSpec.primaryConstructor) val param = constructor.parameters.first { it.name == "response" } @@ -1181,7 +1187,7 @@ class ModelGeneratorTest { val typeSpec = files .first() .members - .filterIsInstance() + .filterIsInstance() .first() val constructor = assertNotNull(typeSpec.primaryConstructor) val param = constructor.parameters.first { it.name == "healthChecks" } @@ -1365,6 +1371,7 @@ class ModelGeneratorTest { ), requestBody = null, responses = emptyMap(), + description = null, ) val apiSpec = ApiSpec( title = "Test", @@ -1402,4 +1409,110 @@ class ModelGeneratorTest { assertTrue(!idParam.type.isNullable, "Required property should be non-nullable") assertEquals(null, idParam.defaultValue, "Required property should have no default") } + + // -- Property KDoc tests (DOCS-03) -- + + @Test + fun `property with description generates KDoc`() { + val schema = SchemaModel( + name = "Pet", + description = null, + properties = listOf( + PropertyModel("name", TypeRef.Primitive(PrimitiveType.STRING), "The pet's name", false), + ), + requiredProperties = setOf("name"), + allOf = null, + oneOf = null, + anyOf = null, + discriminator = null, + ) + val files = generate(spec(schemas = listOf(schema))) + val typeSpec = files + .first() + .members + .filterIsInstance() + .first() + val prop = typeSpec.propertySpecs.first { it.name == "name" } + assertTrue( + prop.kdoc.toString().contains("The pet's name"), + "Expected KDoc with description, got: ${prop.kdoc}", + ) + } + + @Test + fun `property without description generates no KDoc`() { + val schema = SchemaModel( + name = "Pet", + description = null, + properties = listOf( + PropertyModel("name", TypeRef.Primitive(PrimitiveType.STRING), null, false), + ), + requiredProperties = setOf("name"), + allOf = null, + oneOf = null, + anyOf = null, + discriminator = null, + ) + val files = generate(spec(schemas = listOf(schema))) + val typeSpec = files + .first() + .members + .filterIsInstance() + .first() + val prop = typeSpec.propertySpecs.first { it.name == "name" } + assertTrue( + prop.kdoc.toString().isEmpty(), + "Expected no KDoc when description is null, got: ${prop.kdoc}", + ) + } + + @Test + fun `enum with valueDescriptions generates constant KDoc`() { + val enum = EnumModel( + name = "Status", + description = null, + type = EnumBackingType.STRING, + values = listOf(EnumModel.Value("active", "Currently active"), EnumModel.Value("inactive", "Not active")), + ) + val files = generate(spec(enums = listOf(enum))) + val typeSpec = files + .first() + .members + .filterIsInstance() + .first() + val activeConstant = typeSpec.enumConstants["ACTIVE"] + assertNotNull(activeConstant, "Expected ACTIVE enum constant") + assertTrue( + activeConstant.kdoc.toString().contains("Currently active"), + "Expected KDoc 'Currently active' on ACTIVE, got: ${activeConstant.kdoc}", + ) + val inactiveConstant = typeSpec.enumConstants["INACTIVE"] + assertNotNull(inactiveConstant, "Expected INACTIVE enum constant") + assertTrue( + inactiveConstant.kdoc.toString().contains("Not active"), + "Expected KDoc 'Not active' on INACTIVE, got: ${inactiveConstant.kdoc}", + ) + } + + @Test + fun `enum without valueDescriptions generates no constant KDoc`() { + val enum = EnumModel( + name = "Status", + description = null, + type = EnumBackingType.STRING, + values = listOf(EnumModel.Value("active"), EnumModel.Value("inactive")), + ) + val files = generate(spec(enums = listOf(enum))) + val typeSpec = files + .first() + .members + .filterIsInstance() + .first() + val activeConstant = typeSpec.enumConstants["ACTIVE"] + assertNotNull(activeConstant, "Expected ACTIVE enum constant") + assertTrue( + activeConstant.kdoc.toString().isEmpty(), + "Expected no KDoc on ACTIVE when no valueDescriptions, got: ${activeConstant.kdoc}", + ) + } } 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 cb7eaad..e531d94 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 @@ -3,6 +3,7 @@ package com.avsystem.justworks.core.parser import com.avsystem.justworks.core.model.ApiSpec import com.avsystem.justworks.core.model.ContentType import com.avsystem.justworks.core.model.EnumBackingType +import com.avsystem.justworks.core.model.EnumModel import com.avsystem.justworks.core.model.HttpMethod import com.avsystem.justworks.core.model.ParameterLocation import com.avsystem.justworks.core.model.PrimitiveType @@ -54,7 +55,10 @@ class SpecParserTest : SpecParserTestBase() { val petStatus = petstore.enums.find { it.name == "PetStatus" } assertNotNull(petStatus, "PetStatus enum missing") assertEquals(EnumBackingType.STRING, petStatus.type) - assertEquals(listOf("available", "pending", "sold"), petStatus.values) + assertEquals( + listOf(EnumModel.Value("available"), EnumModel.Value("pending"), EnumModel.Value("sold")), + petStatus.values, + ) } @Test @@ -196,10 +200,9 @@ class SpecParserTest : SpecParserTestBase() { val order = spec.schemas.find { it.name == "Order" } ?: fail("Order schema not found") - val itemProp = - order.properties.find { it.name == "item" } - ?: fail("item property not found on Order") - + assert(order.properties.any { it.name == "item" }) { + "item property not found on Order" + } // After resolveFully, the item property may be inlined or a reference // Either way, ItemDetails should exist as a named schema val itemDetails = spec.schemas.find { it.name == "ItemDetails" } @@ -217,7 +220,7 @@ class SpecParserTest : SpecParserTestBase() { // The $ref parameter (LimitParam) should be resolved to an actual parameter val limitParam = listOrders.parameters.find { it.name == "limit" } - ?: fail("limit parameter not found -- \$ref parameter not resolved") + ?: fail($$"limit parameter not found -- $ref parameter not resolved") assertEquals(ParameterLocation.QUERY, limitParam.location) } @@ -307,7 +310,7 @@ class SpecParserTest : SpecParserTestBase() { @Test fun `property with allOf reference resolves to referenced type`() { val spec = - """ + $$""" openapi: 3.0.0 info: title: Test @@ -327,7 +330,7 @@ class SpecParserTest : SpecParserTestBase() { type: string config: allOf: - - ${'$'}ref: '#/components/schemas/TaskConfig' + - $ref: '#/components/schemas/TaskConfig' required: - name """.trimIndent() @@ -421,7 +424,7 @@ class SpecParserTest : SpecParserTestBase() { @Test fun `ref wrapper schema has underlyingType Reference`() { val spec = parseSpec( - """ + $$""" openapi: 3.0.0 info: title: Test @@ -435,7 +438,7 @@ class SpecParserTest : SpecParserTestBase() { name: type: string PetAlias: - ${'$'}ref: '#/components/schemas/Pet' + $ref: '#/components/schemas/Pet' """.trimIndent().toTempFile(), ) @@ -473,7 +476,7 @@ class SpecParserTest : SpecParserTestBase() { @Test fun `schema with allOf has no underlyingType`() { val spec = parseSpec( - """ + $$""" openapi: 3.0.0 info: title: Test @@ -488,7 +491,7 @@ class SpecParserTest : SpecParserTestBase() { type: integer Extended: allOf: - - ${'$'}ref: '#/components/schemas/Base' + - $ref: '#/components/schemas/Base' type: object properties: name: @@ -501,6 +504,77 @@ class SpecParserTest : SpecParserTestBase() { assertEquals(null, extended.underlyingType, "allOf schema should not have underlyingType") } + // -- x-enum-descriptions -- + + @Test + fun `enum with x-enum-descriptions as list populates value descriptions`() { + val spec = parseSpec( + """ + openapi: 3.0.0 + info: + title: Test + version: 1.0.0 + paths: {} + components: + schemas: + Color: + type: string + enum: + - red + - green + - blue + x-enum-descriptions: + - The color red + - The color green + - The color blue + """.trimIndent().toTempFile(), + ) + + val color = spec.enums.find { it.name == "Color" } ?: fail("Color enum not found") + assertEquals( + listOf( + EnumModel.Value("red", "The color red"), + EnumModel.Value("green", "The color green"), + EnumModel.Value("blue", "The color blue"), + ), + color.values, + ) + } + + @Test + fun `enum with x-enum-descriptions as map populates value descriptions`() { + val spec = parseSpec( + """ + openapi: 3.0.0 + info: + title: Test + version: 1.0.0 + paths: {} + components: + schemas: + Priority: + type: string + enum: + - low + - medium + - high + x-enum-descriptions: + low: Low priority + high: High priority + """.trimIndent().toTempFile(), + ) + + val priority = spec.enums.find { it.name == "Priority" } ?: fail("Priority enum not found") + assertEquals( + listOf( + EnumModel.Value("low", "Low priority"), + EnumModel.Value("medium", null), + EnumModel.Value("high", "High priority"), + ), + priority.values, + ) + } + // -- SCHM-03/04/05: Extended format type mapping -- @Test