diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/Utils.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/Utils.kt index 0b5dd4c..0a8b421 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/Utils.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/Utils.kt @@ -5,8 +5,10 @@ import com.avsystem.justworks.core.model.PrimitiveType import com.avsystem.justworks.core.model.PropertyModel import com.avsystem.justworks.core.model.SchemaModel import com.avsystem.justworks.core.model.TypeRef +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BYTE_ARRAY +import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.DOUBLE import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.INT @@ -18,6 +20,12 @@ import com.squareup.kotlinpoet.SET import com.squareup.kotlinpoet.STRING import com.squareup.kotlinpoet.TypeName +internal val DEPRECATED = ClassName("kotlin", "Deprecated") + +/** Builds a `@Deprecated("")` annotation. */ +internal fun deprecatedAnnotation(message: String): AnnotationSpec = + AnnotationSpec.builder(DEPRECATED).addMember("%S", message).build() + internal val TypeRef.properties: List get() = when (this) { is TypeRef.Inline -> properties 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 88469a4..a0a51ca 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 @@ -21,6 +21,7 @@ import com.avsystem.justworks.core.gen.TOKEN 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.deprecatedAnnotation import com.avsystem.justworks.core.gen.invoke import com.avsystem.justworks.core.gen.shared.toAuthParam import com.avsystem.justworks.core.gen.toCamelCase @@ -231,6 +232,10 @@ internal object ClientGenerator { .addModifiers(KModifier.SUSPEND) .returns(returnType) + if (endpoint.deprecated) { + funBuilder.addAnnotation(deprecatedAnnotation("This operation is deprecated.")) + } + val params = endpoint.parameters.groupBy { it.location } val pathParams = params[ParameterLocation.PATH].orEmpty().map { param -> 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 ff54e19..cf53aab 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 @@ -27,6 +27,7 @@ import com.avsystem.justworks.core.gen.UUID_SERIALIZER import com.avsystem.justworks.core.gen.UUID_TYPE import com.avsystem.justworks.core.gen.collectInlineEnums import com.avsystem.justworks.core.gen.collectInlineSchemas +import com.avsystem.justworks.core.gen.deprecatedAnnotation import com.avsystem.justworks.core.gen.invoke import com.avsystem.justworks.core.gen.model.ModelGenerator.buildNestedVariant import com.avsystem.justworks.core.gen.model.ModelGenerator.generateDataClass @@ -165,6 +166,10 @@ internal object ModelGenerator { val parentBuilder = TypeSpec.interfaceBuilder(className).addModifiers(KModifier.SEALED) parentBuilder.addAnnotation(SERIALIZABLE) + if (schema.deprecated) { + parentBuilder.addAnnotation(deprecatedAnnotation("This schema is deprecated.")) + } + if (schema.discriminator != null) { parentBuilder.addAnnotation( AnnotationSpec @@ -264,8 +269,10 @@ internal object ModelGenerator { .builder(kotlinName, type) .initializer(kotlinName) .addAnnotation(AnnotationSpec.builder(SERIAL_NAME).addMember("%S", prop.name).build()) - .apply { prop.description?.let { addKdoc("%L", it) } } - .build() + .apply { + if (prop.deprecated) addAnnotation(deprecatedAnnotation("This property is deprecated.")) + prop.description?.let { addKdoc("%L", it) } + }.build() } builder.primaryConstructor(constructorBuilder.build()) @@ -294,6 +301,10 @@ internal object ModelGenerator { .build(), ) + if (schema.deprecated) { + typeSpec.addAnnotation(deprecatedAnnotation("This schema is deprecated.")) + } + if (schema.description != null) { typeSpec.addKdoc("%L", schema.description) } @@ -423,6 +434,10 @@ internal object ModelGenerator { .addAnnotation(SERIALIZABLE) .addSuperinterfaces(superinterfaces) + if (schema.deprecated) { + typeSpec.addAnnotation(deprecatedAnnotation("This schema is deprecated.")) + } + if (serialName != null) { typeSpec.addAnnotation( AnnotationSpec @@ -664,6 +679,10 @@ internal object ModelGenerator { val typeAlias = TypeAliasSpec.builder(schema.name, primitiveType) + if (schema.deprecated) { + typeAlias.addAnnotation(deprecatedAnnotation("This schema is deprecated.")) + } + if (schema.description != null) { typeAlias.addKdoc("%L", schema.description) } 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 dba29e8..32e498d 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 @@ -42,6 +42,7 @@ data class Endpoint( val parameters: List, val requestBody: RequestBody?, val responses: Map, + val deprecated: Boolean = false, ) enum class HttpMethod { @@ -61,6 +62,7 @@ data class Parameter( val required: Boolean, val schema: TypeRef, val description: String?, + val deprecated: Boolean = false, ) // todo: add cookie @@ -99,6 +101,7 @@ data class SchemaModel( val anyOf: List?, val discriminator: Discriminator?, val underlyingType: TypeRef? = null, + val deprecated: Boolean = false, ) data class PropertyModel( @@ -107,6 +110,7 @@ data class PropertyModel( val description: String?, val nullable: Boolean, val defaultValue: Any? = null, + val deprecated: Boolean = false, ) data class EnumModel( 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 292fd89..314e258 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 @@ -299,6 +299,7 @@ object SpecParser { parameters = mergedParams, requestBody = requestBody, responses = responses, + deprecated = operation.deprecated ?: false, ) } }.toList() @@ -310,6 +311,7 @@ object SpecParser { required = required ?: false, schema = schema?.toTypeRef() ?: TypeRef.Primitive(PrimitiveType.STRING), description = description, + deprecated = deprecated ?: false, ) // --- Schema extraction --- @@ -363,6 +365,7 @@ object SpecParser { anyOf = anyOf?.let { it.map(TypeRef::Reference).ifEmpty { null } }, discriminator = discriminator, underlyingType = underlyingType, + deprecated = schema.deprecated ?: false, ) } @@ -592,6 +595,7 @@ object SpecParser { description = propSchema.description, nullable = propName !in required && !type.honorsDefault(propSchema.default), defaultValue = normalizeDefault(propSchema.default), + deprecated = propSchema.deprecated == true, ) } 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 c98d642..07b4126 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 @@ -116,6 +116,28 @@ class ClientGeneratorTest { assertTrue(KModifier.SUSPEND in funSpec.modifiers, "Expected SUSPEND modifier") } + // -- Deprecated operations -- + + @Test + fun `deprecated operation gets Deprecated annotation`() { + val cls = clientClass(endpoint().copy(deprecated = true)) + val funSpec = cls.funSpecs.first { it.name == "listPets" } + assertTrue( + funSpec.annotations.any { it.typeName.toString() == "kotlin.Deprecated" }, + "Expected @Deprecated on deprecated operation", + ) + } + + @Test + fun `non-deprecated operation has no Deprecated annotation`() { + val cls = clientClass(endpoint()) + val funSpec = cls.funSpecs.first { it.name == "listPets" } + assertFalse( + funSpec.annotations.any { it.typeName.toString() == "kotlin.Deprecated" }, + "Did not expect @Deprecated on non-deprecated operation", + ) + } + // -- CLNT-03: All HTTP methods -- @Test 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 36947fb..3d54ea7 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 @@ -69,6 +69,51 @@ class ModelGeneratorTest { assertTrue(KModifier.DATA in typeSpec.modifiers, "Expected DATA modifier") } + @Test + fun `deprecated schema gets Deprecated annotation on data class`() { + val files = generate(spec(schemas = listOf(petSchema.copy(deprecated = true)))) + val typeSpec = files[0].members.filterIsInstance()[0] + val annotations = typeSpec.annotations.map { it.typeName.toString() } + assertTrue("kotlin.Deprecated" in annotations, "Expected @Deprecated on deprecated schema") + } + + @Test + fun `non-deprecated schema has no Deprecated annotation`() { + val files = generate(spec(schemas = listOf(petSchema))) + val typeSpec = files[0].members.filterIsInstance()[0] + val annotations = typeSpec.annotations.map { it.typeName.toString() } + assertTrue("kotlin.Deprecated" !in annotations, "Did not expect @Deprecated") + } + + @Test + fun `deprecated property gets Deprecated annotation`() { + val schema = SchemaModel( + name = "Thing", + description = null, + properties = listOf( + PropertyModel("legacy", TypeRef.Primitive(PrimitiveType.STRING), null, false, deprecated = true), + PropertyModel("current", TypeRef.Primitive(PrimitiveType.STRING), null, false), + ), + requiredProperties = setOf("legacy", "current"), + allOf = null, + oneOf = null, + anyOf = null, + discriminator = null, + ) + val files = generate(spec(schemas = listOf(schema))) + val typeSpec = files[0].members.filterIsInstance()[0] + val legacy = typeSpec.propertySpecs.first { it.name == "legacy" } + val current = typeSpec.propertySpecs.first { it.name == "current" } + assertTrue( + legacy.annotations.any { it.typeName.toString() == "kotlin.Deprecated" }, + "Expected @Deprecated on deprecated property", + ) + assertTrue( + current.annotations.none { it.typeName.toString() == "kotlin.Deprecated" }, + "Did not expect @Deprecated on normal property", + ) + } + @Test fun `generates class with Serializable annotation`() { val files = generate(spec(schemas = listOf(petSchema))) 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 28cd4b5..14e55d4 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 @@ -141,6 +141,55 @@ class SpecParserTest : SpecParserTestBase() { assertEquals("NewPet", bodyType.schemaName) } + @Test + fun `parses deprecated operations schemas and parameters`() { + val spec = parseSpec( + """ + openapi: 3.0.0 + info: + title: Deprecated API + version: 1.0.0 + paths: + /legacy: + get: + operationId: getLegacy + deprecated: true + parameters: + - name: oldParam + in: query + deprecated: true + schema: + type: string + responses: + '200': + description: ok + components: + schemas: + LegacyModel: + type: object + deprecated: true + properties: + id: + type: string + oldField: + type: string + deprecated: true + """.trimIndent().toTempFile(), + ) + + val op = spec.endpoints.find { it.operationId == "getLegacy" } ?: fail("getLegacy not found") + assertTrue(op.deprecated, "operation should be deprecated") + val param = op.parameters.find { it.name == "oldParam" } ?: fail("oldParam not found") + assertTrue(param.deprecated, "parameter should be deprecated") + + val model = spec.schemas.find { it.name == "LegacyModel" } ?: fail("LegacyModel not found") + assertTrue(model.deprecated, "schema should be deprecated") + val oldField = model.properties.find { it.name == "oldField" } ?: fail("oldField not found") + assertTrue(oldField.deprecated, "property should be deprecated") + val idField = model.properties.find { it.name == "id" } ?: fail("id not found") + assertFalse(idField.deprecated, "normal property should not be deprecated") + } + @Test fun `parsed endpoints have tags`() { val listPets = petstore.endpoints.find { it.operationId == "listPets" }!!