mirror of
https://github.com/jellyfin/jellyfin-sdk-kotlin.git
synced 2024-11-23 05:49:59 +00:00
Generate request models
This commit is contained in:
parent
f8db631fee
commit
2b938a0055
@ -57,6 +57,10 @@ Hooks are classes that will modify the output of the generator. They should be r
|
||||
add a special function called `[operation]Url` (like "GetImageUrl") that will return a string
|
||||
containing the request url.
|
||||
|
||||
- **OperationRequestModelHook**
|
||||
A hook that can request a request model function to be added for an API operation. Compared to the
|
||||
default function it used a data class to contain all parameters.
|
||||
|
||||
- **ServiceNameHook**
|
||||
A hook that can modify the name(s) of an operation. It can add, rename and delete the services
|
||||
for an operation. When all services are removed the operation will never be generated. Can be
|
||||
|
@ -5,6 +5,7 @@ import org.jellyfin.openapi.builder.api.ApiClientExtensionsBuilder
|
||||
import org.jellyfin.openapi.builder.api.ApiNameBuilder
|
||||
import org.jellyfin.openapi.builder.api.ApisBuilder
|
||||
import org.jellyfin.openapi.builder.api.OperationBuilder
|
||||
import org.jellyfin.openapi.builder.api.OperationParameterModelBuilder
|
||||
import org.jellyfin.openapi.builder.api.OperationUrlBuilder
|
||||
import org.jellyfin.openapi.builder.extra.DeprecatedAnnotationSpecBuilder
|
||||
import org.jellyfin.openapi.builder.extra.DescriptionBuilder
|
||||
@ -15,6 +16,7 @@ import org.jellyfin.openapi.builder.model.EnumModelBuilder
|
||||
import org.jellyfin.openapi.builder.model.ModelBuilder
|
||||
import org.jellyfin.openapi.builder.model.ModelsBuilder
|
||||
import org.jellyfin.openapi.builder.model.ObjectModelBuilder
|
||||
import org.jellyfin.openapi.builder.model.RequestModelBuilder
|
||||
import org.jellyfin.openapi.builder.openapi.OpenApiApiServicesBuilder
|
||||
import org.jellyfin.openapi.builder.openapi.OpenApiConstantsBuilder
|
||||
import org.jellyfin.openapi.builder.openapi.OpenApiDefaultValueBuilder
|
||||
@ -40,8 +42,9 @@ val mainModule = module {
|
||||
single { ApiNameBuilder() }
|
||||
single { OperationBuilder(get(), get()) }
|
||||
single { OperationUrlBuilder(get(), get()) }
|
||||
single { ApiBuilder(get(), get(), getAll()) }
|
||||
single { ApisBuilder(get(), get()) }
|
||||
single { OperationParameterModelBuilder(get(), get()) }
|
||||
single { ApiBuilder(get(), get(), get(), get(), getAll(), getAll(), get()) }
|
||||
single { ApisBuilder(get()) }
|
||||
single { ApiClientExtensionsBuilder(get()) }
|
||||
|
||||
// Models
|
||||
@ -50,6 +53,7 @@ val mainModule = module {
|
||||
single { EmptyModelBuilder(get(), get()) }
|
||||
single { EnumModelBuilder(get(), get()) }
|
||||
single { ObjectModelBuilder(get(), get(), get()) }
|
||||
single { RequestModelBuilder(get()) }
|
||||
|
||||
// Files
|
||||
single { FileSpecBuilder() }
|
||||
|
@ -5,32 +5,31 @@ import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import org.jellyfin.openapi.builder.Builder
|
||||
import org.jellyfin.openapi.builder.ContextBuilder
|
||||
import org.jellyfin.openapi.builder.extra.FileSpecBuilder
|
||||
import org.jellyfin.openapi.builder.model.RequestModelBuilder
|
||||
import org.jellyfin.openapi.constants.Classes
|
||||
import org.jellyfin.openapi.constants.Packages
|
||||
import org.jellyfin.openapi.constants.Strings
|
||||
import org.jellyfin.openapi.hooks.OperationRequestModelHook
|
||||
import org.jellyfin.openapi.hooks.OperationUrlHook
|
||||
import org.jellyfin.openapi.model.ApiService
|
||||
import org.jellyfin.openapi.model.ApiServiceOperation
|
||||
import org.jellyfin.openapi.model.GeneratorContext
|
||||
import org.jellyfin.openapi.model.JellyFile
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class ApiBuilder(
|
||||
private val operationBuilder: OperationBuilder,
|
||||
private val operationUrlBuilder: OperationUrlBuilder,
|
||||
private val operationParameterModelBuilder: OperationParameterModelBuilder,
|
||||
private val requestModelBuilder: RequestModelBuilder,
|
||||
private val operationUrlHooks: Collection<OperationUrlHook>,
|
||||
) : Builder<ApiService, JellyFile> {
|
||||
override fun build(data: ApiService): JellyFile = TypeSpec.classBuilder(data.name).apply {
|
||||
// Add "Api" interface as super
|
||||
addSuperinterface(ClassName(Packages.API, Classes.API_INTERFACE))
|
||||
|
||||
// Add "api" value to constructor
|
||||
val apiClientType = ClassName(Packages.API_CLIENT, Classes.API_CLIENT)
|
||||
addProperty(PropertySpec.builder(Strings.API_CLIENT_PARAMETER_NAME, apiClientType, KModifier.PRIVATE)
|
||||
.initializer(Strings.API_CLIENT_PARAMETER_NAME).build())
|
||||
primaryConstructor(FunSpec.constructorBuilder().addParameter(Strings.API_CLIENT_PARAMETER_NAME, apiClientType)
|
||||
.build())
|
||||
|
||||
// Handle deprecated members
|
||||
val operations = data.operations.map { namedOperation ->
|
||||
private val operationRequestModelHooks: Collection<OperationRequestModelHook>,
|
||||
private val fileSpecBuilder: FileSpecBuilder,
|
||||
) : ContextBuilder<ApiService, Unit> {
|
||||
private fun buildAdditionalOperations(operations: Collection<ApiServiceOperation>) =
|
||||
operations.map { namedOperation ->
|
||||
// Check if any member is deprecated
|
||||
if (namedOperation.queryParameters.any { it.deprecated }) {
|
||||
// Return 2 operations, one with and one without deprecated members
|
||||
@ -49,14 +48,55 @@ class ApiBuilder(
|
||||
} else listOf(namedOperation)
|
||||
}.flatten()
|
||||
|
||||
private fun buildTypeSpec(context: GeneratorContext, data: ApiService) = TypeSpec.classBuilder(data.name).apply {
|
||||
// Add "Api" interface as super
|
||||
addSuperinterface(ClassName(Packages.API, Classes.API_INTERFACE))
|
||||
|
||||
// Add "api" value to constructor
|
||||
val apiClientType = ClassName(Packages.API_CLIENT, Classes.API_CLIENT)
|
||||
addProperty(
|
||||
PropertySpec.builder(Strings.API_CLIENT_PARAMETER_NAME, apiClientType, KModifier.PRIVATE)
|
||||
.initializer(Strings.API_CLIENT_PARAMETER_NAME).build()
|
||||
)
|
||||
primaryConstructor(
|
||||
FunSpec.constructorBuilder().addParameter(Strings.API_CLIENT_PARAMETER_NAME, apiClientType)
|
||||
.build()
|
||||
)
|
||||
|
||||
// Create additional operations like the deprecated version
|
||||
val operations = buildAdditionalOperations(data.operations)
|
||||
|
||||
// Add operations
|
||||
operations
|
||||
.sortedBy { it.name }
|
||||
.forEach { namedOperation ->
|
||||
val createRequestModelVariant = operationRequestModelHooks.any {
|
||||
it.shouldOperationBuildRequestModelFun(data, namedOperation)
|
||||
}
|
||||
val createUrlVariant = operationUrlHooks.any {
|
||||
it.shouldOperationBuildUrlFun(data, namedOperation)
|
||||
}
|
||||
|
||||
// Default variant
|
||||
addFunction(operationBuilder.build(namedOperation))
|
||||
|
||||
if (operationUrlHooks.any { it.shouldOperationBuildUrlFun(data, namedOperation) })
|
||||
addFunction(operationUrlBuilder.build(namedOperation))
|
||||
// Request model variant
|
||||
if (createRequestModelVariant) {
|
||||
context += fileSpecBuilder.build(requestModelBuilder.build(namedOperation))
|
||||
addFunction(operationParameterModelBuilder.build(namedOperation))
|
||||
}
|
||||
|
||||
// URL variant
|
||||
if (createUrlVariant) addFunction(operationUrlBuilder.build(namedOperation))
|
||||
}
|
||||
}.build().let { JellyFile(Packages.API, emptySet(), it) }
|
||||
}
|
||||
|
||||
override fun build(context: GeneratorContext, data: ApiService) {
|
||||
val api = buildTypeSpec(context, data)
|
||||
.build()
|
||||
.let { JellyFile(Packages.API, emptySet(), it) }
|
||||
.let { fileSpecBuilder.build(it) }
|
||||
|
||||
context += api
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,13 @@
|
||||
package org.jellyfin.openapi.builder.api
|
||||
|
||||
import org.jellyfin.openapi.builder.ContextBuilder
|
||||
import org.jellyfin.openapi.builder.extra.FileSpecBuilder
|
||||
import org.jellyfin.openapi.model.ApiService
|
||||
import org.jellyfin.openapi.model.GeneratorContext
|
||||
|
||||
class ApisBuilder(
|
||||
private val apiBuilder: ApiBuilder,
|
||||
private val fileSpecBuilder: FileSpecBuilder,
|
||||
) : ContextBuilder<Collection<ApiService>, Unit> {
|
||||
override fun build(context: GeneratorContext, data: Collection<ApiService>) {
|
||||
for (apiService in data) {
|
||||
val file = apiBuilder.build(apiService)
|
||||
context += fileSpecBuilder.build(file)
|
||||
}
|
||||
for (apiService in data) apiBuilder.build(context, apiService)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
package org.jellyfin.openapi.builder.api
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.ParameterSpec
|
||||
import com.squareup.kotlinpoet.buildCodeBlock
|
||||
import com.squareup.kotlinpoet.withIndent
|
||||
import net.pearx.kasechange.toPascalCase
|
||||
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.ApiServiceOperation
|
||||
import org.jellyfin.openapi.model.ApiServiceOperationParameter
|
||||
import org.jellyfin.openapi.model.DefaultValue
|
||||
|
||||
class OperationParameterModelBuilder(
|
||||
descriptionBuilder: DescriptionBuilder,
|
||||
deprecatedAnnotationSpecBuilder: DeprecatedAnnotationSpecBuilder,
|
||||
) : OperationBuilder(descriptionBuilder, deprecatedAnnotationSpecBuilder) {
|
||||
private fun FunSpec.Builder.addOperationCall(
|
||||
operationName: String,
|
||||
requestParameterName: String,
|
||||
parameters: Collection<String>,
|
||||
includeData: Boolean,
|
||||
) = buildCodeBlock {
|
||||
require(parameters.isNotEmpty()) { "At least 1 parameter expected to generate a valid function call" }
|
||||
|
||||
add("return·%N(\n", operationName)
|
||||
withIndent {
|
||||
parameters.forEach { parameter ->
|
||||
addStatement("%1N·=·%2N.%1N,", parameter, requestParameterName)
|
||||
}
|
||||
if (includeData) addStatement("%1N·=·%1N,", "data")
|
||||
}
|
||||
add(")\n")
|
||||
}.let(::addCode)
|
||||
|
||||
override fun build(data: ApiServiceOperation): FunSpec = buildFunctionShell(data).apply {
|
||||
val requestParameterType = ClassName(
|
||||
Packages.MODEL_REQUEST,
|
||||
data.name.toPascalCase() + Strings.MODEL_REQUEST_SUFFIX
|
||||
)
|
||||
val parameters = data.pathParameters + data.queryParameters
|
||||
|
||||
ParameterSpec.builder(Strings.MODEL_REQUEST_PARAMETER_NAME, requestParameterType).apply {
|
||||
addKdoc("%L", Strings.MODEL_REQUEST_PARAMETER_DESCRIPTION)
|
||||
|
||||
// Add default value is all parameters have a default
|
||||
if (parameters.all { it.containsDefault() }) defaultValue(
|
||||
"%T()",
|
||||
requestParameterType
|
||||
)
|
||||
}.build().let(::addParameter)
|
||||
|
||||
val includeData = data.bodyType != null
|
||||
if (includeData) {
|
||||
addParameter(ParameterSpec.builder("data", data.bodyType!!).apply {
|
||||
// Set default value to null if parameter is nullable
|
||||
if (data.bodyType.isNullable) defaultValue("%L", "null")
|
||||
}.build())
|
||||
}
|
||||
|
||||
addOperationCall(data.name, Strings.MODEL_REQUEST_PARAMETER_NAME, parameters.map { it.name }, includeData)
|
||||
}.build()
|
||||
|
||||
private fun ApiServiceOperationParameter.containsDefault(): Boolean {
|
||||
if (type.isNullable) return true
|
||||
if (defaultValue is DefaultValue.Conditional) return defaultValue.modelValue != null
|
||||
if (defaultValue != null) return true
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ class OperationUrlBuilder(
|
||||
private val deprecatedAnnotationSpecBuilder: DeprecatedAnnotationSpecBuilder,
|
||||
) : OperationBuilder(descriptionBuilder, deprecatedAnnotationSpecBuilder) {
|
||||
override fun buildFunctionShell(data: ApiServiceOperation) = FunSpec.builder(
|
||||
data.name + Strings.URL_OPERATION_SUFFIX
|
||||
buildFunctionName(data.name)
|
||||
).apply {
|
||||
// Add description
|
||||
descriptionBuilder.build(data.description)?.let {
|
||||
@ -27,6 +27,8 @@ class OperationUrlBuilder(
|
||||
returns(Types.STRING)
|
||||
}
|
||||
|
||||
fun buildFunctionName(name: String) = name + Strings.URL_OPERATION_SUFFIX
|
||||
|
||||
override fun build(data: ApiServiceOperation): FunSpec = buildFunctionShell(data).apply {
|
||||
// Add parameters receivers
|
||||
data.pathParameters
|
||||
|
@ -30,6 +30,7 @@ fun ParameterSpec.Builder.defaultValue(
|
||||
is DefaultValue.Boolean -> defaultValue("%L", defaultValue.value)
|
||||
is DefaultValue.EnumMember -> defaultValue("%T.%L", defaultValue.enumType, defaultValue.memberName)
|
||||
is DefaultValue.CodeBlock -> defaultValue(defaultValue.build())
|
||||
is DefaultValue.Conditional -> defaultValue(type, defaultValue.parameterValue, allowEmptyCollection)
|
||||
// Set value to null by default for nullable values
|
||||
null -> when {
|
||||
typeClassName == Types.COLLECTION && allowEmptyCollection ->
|
||||
|
@ -0,0 +1,38 @@
|
||||
package org.jellyfin.openapi.builder.model
|
||||
|
||||
import net.pearx.kasechange.toPascalCase
|
||||
import org.jellyfin.openapi.builder.Builder
|
||||
import org.jellyfin.openapi.constants.Packages
|
||||
import org.jellyfin.openapi.constants.Strings
|
||||
import org.jellyfin.openapi.model.ApiServiceOperation
|
||||
import org.jellyfin.openapi.model.DefaultValue
|
||||
import org.jellyfin.openapi.model.JellyFile
|
||||
import org.jellyfin.openapi.model.ObjectApiModel
|
||||
import org.jellyfin.openapi.model.ObjectApiModelProperty
|
||||
|
||||
class RequestModelBuilder(
|
||||
private val objectModelBuilder: ObjectModelBuilder,
|
||||
) : Builder<ApiServiceOperation, JellyFile> {
|
||||
override fun build(data: ApiServiceOperation): JellyFile = objectModelBuilder.build(
|
||||
ObjectApiModel(
|
||||
data.name.toPascalCase() + Strings.MODEL_REQUEST_SUFFIX,
|
||||
data.description,
|
||||
false,
|
||||
(data.pathParameters + data.queryParameters).map { parameter ->
|
||||
ObjectApiModelProperty(
|
||||
name = parameter.name,
|
||||
originalName = parameter.originalName,
|
||||
type = parameter.type,
|
||||
defaultValue = when (parameter.defaultValue) {
|
||||
is DefaultValue.Conditional -> parameter.defaultValue.modelValue
|
||||
else -> parameter.defaultValue
|
||||
},
|
||||
description = parameter.description,
|
||||
deprecated = parameter.deprecated,
|
||||
)
|
||||
}.toSet()
|
||||
)
|
||||
).copy(
|
||||
namespace = Packages.MODEL_REQUEST,
|
||||
)
|
||||
}
|
@ -65,9 +65,21 @@ class OpenApiApiServicesBuilder(
|
||||
path: ApiTypePath,
|
||||
type: TypeName,
|
||||
parameterSpec: Parameter,
|
||||
): DefaultValue? =
|
||||
defaultValueHooks.firstNotNullOfOrNull { hook -> hook.onBuildDefaultValue(path, type, parameterSpec) }
|
||||
?: openApiDefaultValueBuilder.build(context, parameterSpec.schema)
|
||||
): DefaultValue? {
|
||||
val hookDefault = defaultValueHooks.firstNotNullOfOrNull { hook ->
|
||||
hook.onBuildDefaultValue(path, type, parameterSpec)
|
||||
}
|
||||
val generatorDefault = openApiDefaultValueBuilder.build(context, parameterSpec.schema)
|
||||
|
||||
if (hookDefault is DefaultValue.Conditional) {
|
||||
return hookDefault.copy(
|
||||
modelValue = hookDefault.modelValue ?: generatorDefault,
|
||||
parameterValue = hookDefault.parameterValue ?: generatorDefault,
|
||||
)
|
||||
}
|
||||
|
||||
return generatorDefault
|
||||
}
|
||||
|
||||
private fun buildValidation(schema: Schema<*>): ParameterValidation? = when {
|
||||
schema is IntegerSchema && schema.minimum != null && schema.maximum != null -> IntRangeValidation(
|
||||
|
@ -36,6 +36,11 @@ object Packages {
|
||||
*/
|
||||
const val MODEL = "org.jellyfin.sdk.model.api"
|
||||
|
||||
/**
|
||||
* Package for the generated request models
|
||||
*/
|
||||
const val MODEL_REQUEST = "org.jellyfin.sdk.model.api.request"
|
||||
|
||||
/**
|
||||
* Package containing all kotlinx.serialization serializers
|
||||
*/
|
||||
|
@ -51,4 +51,19 @@ object Strings {
|
||||
* Name of the parameter used to reference the HttpClient instance in generated api classes.
|
||||
*/
|
||||
const val API_CLIENT_PARAMETER_NAME = "api"
|
||||
|
||||
/**
|
||||
* The suffix added to generated models containing operation parameters.
|
||||
*/
|
||||
const val MODEL_REQUEST_SUFFIX = "Request"
|
||||
|
||||
/**
|
||||
* Name of the parameter used for the generated request model containing operation parameters.
|
||||
*/
|
||||
const val MODEL_REQUEST_PARAMETER_NAME = "request"
|
||||
|
||||
/**
|
||||
* Description of the parameter used for the generated request model containing operation parameters.
|
||||
*/
|
||||
const val MODEL_REQUEST_PARAMETER_DESCRIPTION = "The request paramaters"
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.jellyfin.openapi.hooks
|
||||
|
||||
import org.jellyfin.openapi.hooks.api.BinaryOperationUrlHook
|
||||
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.ImageMapsHook
|
||||
@ -16,6 +17,8 @@ val hooksModule = module {
|
||||
single { BinaryOperationUrlHook() } bind OperationUrlHook::class
|
||||
single { ClientLogOperationUrlHook() } bind OperationUrlHook::class
|
||||
|
||||
single { LargeOperationRequestModelHook() } bind OperationRequestModelHook::class
|
||||
|
||||
single { PlayStateServiceNameHook() } bind ServiceNameHook::class
|
||||
|
||||
single { DefaultUserIdHook() } bind DefaultValueHook::class
|
||||
|
@ -0,0 +1,11 @@
|
||||
package org.jellyfin.openapi.hooks
|
||||
|
||||
import org.jellyfin.openapi.model.ApiService
|
||||
import org.jellyfin.openapi.model.ApiServiceOperation
|
||||
|
||||
interface OperationRequestModelHook {
|
||||
/**
|
||||
* Determine if a function should be added to request an endpoint with a model instead of parameters.
|
||||
*/
|
||||
fun shouldOperationBuildRequestModelFun(api: ApiService, operation: ApiServiceOperation): Boolean
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.jellyfin.openapi.hooks.api
|
||||
|
||||
import org.jellyfin.openapi.hooks.OperationRequestModelHook
|
||||
import org.jellyfin.openapi.model.ApiService
|
||||
import org.jellyfin.openapi.model.ApiServiceOperation
|
||||
|
||||
class LargeOperationRequestModelHook : OperationRequestModelHook {
|
||||
companion object {
|
||||
const val PARAMETER_TRESHOLD = 5
|
||||
}
|
||||
|
||||
override fun shouldOperationBuildRequestModelFun(api: ApiService, operation: ApiServiceOperation): Boolean =
|
||||
(operation.queryParameters.size + operation.pathParameters.size) >= PARAMETER_TRESHOLD
|
||||
}
|
@ -6,6 +6,7 @@ import org.jellyfin.openapi.constants.Types
|
||||
import org.jellyfin.openapi.hooks.ApiTypePath
|
||||
import org.jellyfin.openapi.hooks.DefaultValueHook
|
||||
import org.jellyfin.openapi.model.CurrentUserIdDefaultValue
|
||||
import org.jellyfin.openapi.model.DefaultValue
|
||||
|
||||
class DefaultUserIdHook : DefaultValueHook {
|
||||
companion object {
|
||||
@ -20,8 +21,8 @@ class DefaultUserIdHook : DefaultValueHook {
|
||||
// Check for name and nullability
|
||||
path.parameter in VALID_NAMES && !type.isNullable -> when (type) {
|
||||
// Check for supported types
|
||||
Types.UUID -> CurrentUserIdDefaultValue(false)
|
||||
Types.STRING -> CurrentUserIdDefaultValue(true)
|
||||
Types.UUID -> DefaultValue.Conditional(parameterValue = CurrentUserIdDefaultValue(false))
|
||||
Types.STRING -> DefaultValue.Conditional(parameterValue = CurrentUserIdDefaultValue(true))
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
|
@ -29,4 +29,9 @@ sealed interface DefaultValue {
|
||||
interface CodeBlock : DefaultValue {
|
||||
fun build(): com.squareup.kotlinpoet.CodeBlock
|
||||
}
|
||||
|
||||
data class Conditional(
|
||||
val modelValue: DefaultValue? = null,
|
||||
val parameterValue: DefaultValue? = null,
|
||||
) : DefaultValue
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user