Implement DescriptionHook for more advanced description customization

This commit is contained in:
Niels van Velzen 2022-09-21 22:00:39 +02:00 committed by Max Rumpf
parent cb793a9362
commit 65cff81b06
14 changed files with 106 additions and 31 deletions

View File

@ -72,6 +72,10 @@ Hooks are classes that will modify the output of the generator. They should be r
instance of `CustomDefaultValue` that contains the code builder. The generator will use the
default value from the JSON schema if all hooks return null.
- **DescriptionHook**
A hook that allows customization of generated descriptions for operations, parameters, models and
properties. The hook returns the new description or null if the description must be removed.
## Phases
The conversion happens in multiple phases. The phases in order are:

View File

@ -61,5 +61,5 @@ val mainModule = module {
// Utilities
single { DeprecatedAnnotationSpecBuilder() }
single { TypeSerializerBuilder() }
single { DescriptionBuilder() }
single { DescriptionBuilder(getAll()) }
}

View File

@ -15,9 +15,9 @@ import org.jellyfin.openapi.constants.Classes
import org.jellyfin.openapi.constants.Packages
import org.jellyfin.openapi.constants.Strings
import org.jellyfin.openapi.constants.Types
import org.jellyfin.openapi.model.DescriptionType
import org.jellyfin.openapi.model.ApiServiceOperation
import org.jellyfin.openapi.model.ApiServiceOperationParameter
import org.jellyfin.openapi.model.DefaultValue
import org.jellyfin.openapi.model.IntRangeValidation
import org.jellyfin.openapi.model.ParameterValidation
@ -30,7 +30,7 @@ open class OperationBuilder(
addModifiers(KModifier.SUSPEND)
// Add description
descriptionBuilder.build(data.description)?.let {
descriptionBuilder.build(DescriptionType.OPERATION, data.description)?.let {
addKdoc("%L", it)
}
@ -47,7 +47,7 @@ open class OperationBuilder(
defaultValue(data)
// Add description
descriptionBuilder.build(data.description)?.let {
descriptionBuilder.build(DescriptionType.OPERATION_PARAMETER, data.description)?.let {
addKdoc("%L", it)
}
}.build()

View File

@ -10,12 +10,13 @@ import org.jellyfin.openapi.builder.extra.DeprecatedAnnotationSpecBuilder
import org.jellyfin.openapi.builder.extra.DescriptionBuilder
import org.jellyfin.openapi.constants.Packages
import org.jellyfin.openapi.constants.Strings
import org.jellyfin.openapi.model.DescriptionType
import org.jellyfin.openapi.model.ApiServiceOperation
import org.jellyfin.openapi.model.ApiServiceOperationParameter
import org.jellyfin.openapi.model.DefaultValue
class OperationParameterModelBuilder(
descriptionBuilder: DescriptionBuilder,
private val descriptionBuilder: DescriptionBuilder,
deprecatedAnnotationSpecBuilder: DeprecatedAnnotationSpecBuilder,
) : OperationBuilder(descriptionBuilder, deprecatedAnnotationSpecBuilder) {
private fun FunSpec.Builder.addOperationCall(
@ -44,7 +45,9 @@ class OperationParameterModelBuilder(
val parameters = data.pathParameters + data.queryParameters
ParameterSpec.builder(Strings.MODEL_REQUEST_PARAMETER_NAME, requestParameterType).apply {
addKdoc("%L", Strings.MODEL_REQUEST_PARAMETER_DESCRIPTION)
descriptionBuilder.build(DescriptionType.OPERATION_PARAMETER, Strings.MODEL_REQUEST_PARAMETER_DESCRIPTION)?.let {
addKdoc("%L", it)
}
// Add default value is all parameters have a default
if (parameters.all { it.containsDefault() }) defaultValue(

View File

@ -6,6 +6,7 @@ import org.jellyfin.openapi.builder.extra.DeprecatedAnnotationSpecBuilder
import org.jellyfin.openapi.builder.extra.DescriptionBuilder
import org.jellyfin.openapi.constants.Strings
import org.jellyfin.openapi.constants.Types
import org.jellyfin.openapi.model.DescriptionType
import org.jellyfin.openapi.model.ApiServiceOperation
class OperationUrlBuilder(
@ -16,7 +17,7 @@ class OperationUrlBuilder(
buildFunctionName(data.name)
).apply {
// Add description
descriptionBuilder.build(data.description)?.let {
descriptionBuilder.build(DescriptionType.OPERATION, data.description)?.let {
addKdoc("%L", it)
}
@ -37,7 +38,9 @@ class OperationUrlBuilder(
ParameterSpec.builder("includeCredentials", Types.BOOLEAN).apply {
defaultValue("%L", data.requireAuthentication)
addKdoc("%L", Strings.INCLUDE_CREDENTIALS_DESCRIPTION)
descriptionBuilder.build(DescriptionType.OPERATION_PARAMETER, Strings.INCLUDE_CREDENTIALS_DESCRIPTION)?.let {
addKdoc("%L", it)
}
}.build().let(::addParameter)
// Create parameter maps

View File

@ -1,26 +1,20 @@
package org.jellyfin.openapi.builder.extra
import org.jellyfin.openapi.builder.Builder
import org.jellyfin.openapi.hooks.DescriptionHook
import org.jellyfin.openapi.model.DescriptionType
class DescriptionBuilder : Builder<String?, String?> {
private val replacements = mapOf(
// Replace CRLF with LF
Regex("""\r\n?""") to "\n",
// Replace <br /> elements with new line
Regex("""<br\s?/?>""") to "\n",
// Replace <seealso> elements with their value
Regex("""<seealso\s+cref="(.*?)"\s?/>""") to "`$1`",
Regex("""<seealso\s+cref="(.*?)"\s?>(.*?)</see>""") to "$2 (`$1`)",
// Replace <see> elements with their value
Regex("""<see\s+cref="(.*?)"\s?/>""") to "`$1`",
Regex("""<see\s+cref="(.*?)"\s?>(.*?)</see>""") to "$2 (`$1`)",
)
class DescriptionBuilder(
private val descriptionsHooks: Collection<DescriptionHook>,
) {
fun build(type: DescriptionType, description: String?): String? {
if (description == null) return null
override fun build(data: String?): String? {
if (data == null) return null
return replacements.entries.fold(data) { acc, (search, replace) ->
acc.replace(search, replace)
var modifiedDescription: String? = description
for (hook in descriptionsHooks) {
if (modifiedDescription == null) return null
modifiedDescription = hook.modify(type, modifiedDescription)
}
return modifiedDescription
}
}

View File

@ -9,6 +9,7 @@ import org.jellyfin.openapi.builder.extra.DescriptionBuilder
import org.jellyfin.openapi.constants.Packages
import org.jellyfin.openapi.constants.Strings
import org.jellyfin.openapi.constants.Types
import org.jellyfin.openapi.model.DescriptionType
import org.jellyfin.openapi.model.EmptyApiModel
import org.jellyfin.openapi.model.JellyFile
@ -19,7 +20,7 @@ class EmptyModelBuilder(
override fun build(data: EmptyApiModel): JellyFile {
return TypeSpec.classBuilder(data.name.toPascalCase(from = CaseFormat.CAPITALIZED_CAMEL))
.apply {
descriptionBuilder.build(data.description)?.let {
descriptionBuilder.build(DescriptionType.MODEL, data.description)?.let {
addKdoc("%L", it)
}
if (data.deprecated) addAnnotation(deprecatedAnnotationSpecBuilder.build(Strings.DEPRECATED_CLASS))

View File

@ -14,6 +14,7 @@ import org.jellyfin.openapi.builder.extra.DescriptionBuilder
import org.jellyfin.openapi.constants.Packages
import org.jellyfin.openapi.constants.Strings
import org.jellyfin.openapi.constants.Types
import org.jellyfin.openapi.model.DescriptionType
import org.jellyfin.openapi.model.EnumApiModel
import org.jellyfin.openapi.model.JellyFile
@ -51,7 +52,7 @@ class EnumModelBuilder(
}
// Header
descriptionBuilder.build(data.description)?.let {
descriptionBuilder.build(DescriptionType.MODEL, data.description)?.let {
addKdoc("%L", it)
}
if (data.deprecated) addAnnotation(deprecatedAnnotationSpecBuilder.build(Strings.DEPRECATED_CLASS))

View File

@ -17,6 +17,7 @@ import org.jellyfin.openapi.builder.extra.defaultValue
import org.jellyfin.openapi.constants.Packages
import org.jellyfin.openapi.constants.Strings
import org.jellyfin.openapi.constants.Types
import org.jellyfin.openapi.model.DescriptionType
import org.jellyfin.openapi.model.JellyFile
import org.jellyfin.openapi.model.ObjectApiModel
@ -41,7 +42,7 @@ class ObjectModelBuilder(
.builder(property.name, property.type)
.initializer(property.name)
.apply {
descriptionBuilder.build(property.description)?.let {
descriptionBuilder.build(DescriptionType.MODEL_PROPERTY, property.description)?.let {
addKdoc("%L", it)
}
@ -76,7 +77,7 @@ class ObjectModelBuilder(
return TypeSpec.classBuilder(data.name.toPascalCase(from = CaseFormat.CAPITALIZED_CAMEL))
.apply {
modifiers += KModifier.DATA
descriptionBuilder.build(data.description)?.let {
descriptionBuilder.build(DescriptionType.MODEL, data.description)?.let {
addKdoc("%L", it)
}
if (data.deprecated) addAnnotation(deprecatedAnnotationSpecBuilder.build(Strings.DEPRECATED_CLASS))

View File

@ -0,0 +1,12 @@
package org.jellyfin.openapi.hooks
import org.jellyfin.openapi.model.DescriptionType
interface DescriptionHook {
/**
* Modify or drop a given description for API models and operations.
*
* @return The new description, null means removing the description.
*/
fun modify(type: DescriptionType, description: String): String?
}

View File

@ -5,7 +5,9 @@ import org.jellyfin.openapi.hooks.api.ClientLogOperationUrlHook
import org.jellyfin.openapi.hooks.api.LargeOperationRequestModelHook
import org.jellyfin.openapi.hooks.api.PlayStateServiceNameHook
import org.jellyfin.openapi.hooks.model.DefaultUserIdHook
import org.jellyfin.openapi.hooks.model.DotNetDescriptionHook
import org.jellyfin.openapi.hooks.model.ImageMapsHook
import org.jellyfin.openapi.hooks.model.SwashbuckleDescriptionHook
import org.jellyfin.openapi.hooks.model.SyncPlayGroupUpdateHook
import org.koin.dsl.bind
import org.koin.dsl.module
@ -22,4 +24,7 @@ val hooksModule = module {
single { PlayStateServiceNameHook() } bind ServiceNameHook::class
single { DefaultUserIdHook() } bind DefaultValueHook::class
single { DotNetDescriptionHook() } bind DescriptionHook::class
single { SwashbuckleDescriptionHook() } bind DescriptionHook::class
}

View File

@ -0,0 +1,19 @@
package org.jellyfin.openapi.hooks.model
import org.jellyfin.openapi.hooks.DescriptionHook
import org.jellyfin.openapi.model.DescriptionType
class DotNetDescriptionHook : DescriptionHook {
private companion object {
private val getsOrSetsRegex = Regex("""^(Gets or sets|Gets|Sets)\s+(.*)$""")
}
override fun modify(type: DescriptionType, description: String): String {
// Ignore API operations
if (type == DescriptionType.OPERATION) return description
return description.replace(getsOrSetsRegex) { result ->
result.groupValues[2].replaceFirstChar(Char::uppercase)
}
}
}

View File

@ -0,0 +1,24 @@
package org.jellyfin.openapi.hooks.model
import org.jellyfin.openapi.hooks.DescriptionHook
import org.jellyfin.openapi.model.DescriptionType
class SwashbuckleDescriptionHook : DescriptionHook {
private companion object {
private val replacements = mapOf(
// Replace CRLF with LF
Regex("""\r\n?""") to "\n",
// Replace <br /> elements with new line
Regex("""<br\s?/?>""") to "\n",
// Replace <seealso> elements with their value
Regex("""<seealso\s+cref="(.*?)"\s?/>""") to "`$1`",
Regex("""<seealso\s+cref="(.*?)"\s?>(.*?)</see>""") to "$2 (`$1`)",
// Replace <see> elements with their value
Regex("""<see\s+cref="(.*?)"\s?/>""") to "`$1`",
Regex("""<see\s+cref="(.*?)"\s?>(.*?)</see>""") to "$2 (`$1`)",
)
}
override fun modify(type: DescriptionType, description: String): String = replacements.entries
.fold(description) { acc, (search, replace) -> acc.replace(search, replace) }
}

View File

@ -0,0 +1,8 @@
package org.jellyfin.openapi.model
enum class DescriptionType {
OPERATION,
OPERATION_PARAMETER,
MODEL,
MODEL_PROPERTY,
}