diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 5eb63a05c..f76a31f36 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -1,378 +1,360 @@ package com.larsreimann.api_editor.codegen -import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.Boundary +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS +import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED import com.larsreimann.api_editor.model.PythonParameterAssignment.IMPLICIT import com.larsreimann.api_editor.model.PythonParameterAssignment.NAME_ONLY import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_ONLY import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_OR_NAME import com.larsreimann.api_editor.mutable_model.PythonArgument +import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonBoolean import com.larsreimann.api_editor.mutable_model.PythonCall import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonConstructor +import com.larsreimann.api_editor.mutable_model.PythonDeclaration import com.larsreimann.api_editor.mutable_model.PythonEnum +import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonExpression import com.larsreimann.api_editor.mutable_model.PythonFloat import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonInt import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType +import com.larsreimann.api_editor.mutable_model.PythonType +import com.larsreimann.modeling.closest +import com.larsreimann.modeling.descendants + +/* ******************************************************************************************************************** + * Declarations + * ********************************************************************************************************************/ -/** - * Builds a string containing the formatted module content - * @receiver The module whose adapter content should be built - * @return The string containing the formatted module content - */ fun PythonModule.toPythonCode(): String { - var formattedImport = buildNamespace(this) - var formattedEnums = enums.joinToString("\n") { it.toPythonCode() } - var formattedClasses = buildAllClasses(this) - var formattedFunctions = buildAllFunctions(this) - val separators = buildSeparators( - formattedImport, formattedClasses, formattedFunctions + val strings = listOf( + importsToPythonCode(), + classes.joinToString("\n\n") { it.toPythonCode() }, + functions.joinToString("\n\n") { it.toPythonCode() }, + enums.joinToString("\n\n") { it.toPythonCode() } ) - formattedImport += separators[0] - if (formattedEnums.isNotBlank()) { - formattedEnums += "\n" - } - formattedClasses += separators[1] - formattedFunctions += separators[2] - return ( - formattedImport + - formattedEnums + - formattedClasses + - formattedFunctions - ) + + val joinedStrings = strings + .filter { it.isNotBlank() } + .joinToString("\n\n") + + return "$joinedStrings\n" } -private fun buildNamespace(pythonModule: PythonModule): String { - val importedModules = HashSet() - pythonModule.functions.forEach { pythonFunction: PythonFunction -> - importedModules.add( - buildParentDeclarationName(pythonFunction.callToOriginalAPI!!.receiver) - ) +private fun PythonModule.importsToPythonCode() = buildString { + val imports = buildSet { + + // Functions + this += descendants { it !is PythonDeclaration } + .filterIsInstance() + .filter { !it.isMethod() || it.isStaticMethod() } + .mapNotNull { + when (val receiver = it.callToOriginalAPI?.receiver) { + is PythonStringifiedExpression -> receiver.string.parentQualifiedName() + else -> null + } + } + + // Constructors + this += descendants() + .filterIsInstance() + .mapNotNull { + when (val receiver = it.callToOriginalAPI?.receiver) { + is PythonStringifiedExpression -> receiver.string.parentQualifiedName() + else -> null + } + } + } + + val importsString = imports.joinToString("\n") { "import $it" } + var fromImportsString = "from __future__ import annotations" + if (enums.isNotEmpty()) { + fromImportsString += "\nfrom enum import Enum" } - pythonModule.classes.forEach { pythonClass: PythonClass -> - importedModules.add( - buildParentDeclarationName(pythonClass.originalClass!!.qualifiedName) - ) + + if (importsString.isNotBlank()) { + append(importsString) + + if (fromImportsString.isNotBlank()) { + append("\n\n") + } } - var result = importedModules.joinToString("\n") { "import $it" } - if (pythonModule.enums.isNotEmpty()) { - result = "from enum import Enum\n$result" + if (fromImportsString.isNotBlank()) { + append(fromImportsString) } - return result } -private fun buildParentDeclarationName(qualifiedName: String): String { +private fun String.parentQualifiedName(): String { val pathSeparator = "." - val separationPosition = qualifiedName.lastIndexOf(pathSeparator) - return qualifiedName.substring(0, separationPosition) + val separationPosition = lastIndexOf(pathSeparator) + return substring(0, separationPosition) } -private fun buildAllClasses(pythonModule: PythonModule): String { - return pythonModule.classes.joinToString("\n".repeat(2)) { it.toPythonCode() } +internal fun PythonAttribute.toPythonCode() = buildString { + append("self.$name") + type?.toPythonCodeOrNull()?.let { + append(": $it") + } + value?.toPythonCode()?.let { + append(" = $it") + } } -private fun buildAllFunctions(pythonModule: PythonModule): String { - return pythonModule.functions.joinToString("\n".repeat(2)) { it.toPythonCode() } -} +internal fun PythonClass.toPythonCode() = buildString { + val constructorString = constructor?.toPythonCode() ?: "" + val methodsString = methods.joinToString("\n\n") { it.toPythonCode() } -private fun buildSeparators( - formattedImports: String, - formattedClasses: String, - formattedFunctions: String -): Array { - val importSeparator: String = if (formattedImports.isBlank()) { - "" - } else if (formattedClasses.isBlank() && formattedFunctions.isBlank()) { - "\n" - } else { - "\n\n" + appendLine("class $name:") + if (constructorString.isNotBlank()) { + appendIndented(constructorString) + if (methodsString.isNotBlank()) { + append("\n\n") + } } - val classesSeparator: String = if (formattedClasses.isBlank()) { - "" - } else if (formattedFunctions.isBlank()) { - "\n" - } else { - "\n\n" + if (methodsString.isNotBlank()) { + appendIndented(methodsString) } - val functionSeparator: String = if (formattedFunctions.isBlank()) { - "" - } else { - "\n" + if (constructorString.isBlank() && methodsString.isBlank()) { + appendIndented("pass") } - return arrayOf(importSeparator, classesSeparator, functionSeparator) } -/** - * Builds a string containing the formatted class content - * @receiver The module whose adapter content should be built - * @return The string containing the formatted class content - */ -fun PythonClass.toPythonCode(): String { - var formattedClass = "class $name:\n" - if (constructor != null) { - formattedClass += buildConstructor(this).prependIndent(" ") +internal fun PythonConstructor.toPythonCode() = buildString { + val parametersString = parameters.toPythonCode() + val boundariesString = parameters + .mapNotNull { it.boundary?.toPythonCode(it.name) } + .joinToString("\n") + val attributesString = closest() + ?.attributes + ?.joinToString("\n") { it.toPythonCode() } + ?: "" + val callString = callToOriginalAPI + ?.let { "self.instance = ${it.toPythonCode()}" } + ?: "" + + appendLine("def __init__($parametersString):") + if (boundariesString.isNotBlank()) { + appendIndented(boundariesString) + if (attributesString.isNotBlank() || callString.isNotBlank()) { + append("\n\n") + } } - if (!methods.isEmpty()) { - if (constructor != null) { - formattedClass += "\n\n" + if (attributesString.isNotBlank()) { + appendIndented(attributesString) + if (callString.isNotBlank()) { + append("\n\n") } - formattedClass += buildAllFunctions(this).joinToString("\n".repeat(2)) } - return formattedClass -} - -private fun buildAllFunctions(pythonClass: PythonClass): List { - return pythonClass.methods.map { it.toPythonCode().prependIndent(" ") } + if (callString.isNotBlank()) { + appendIndented(callString) + } + if (boundariesString.isBlank() && attributesString.isBlank() && callString.isBlank()) { + appendIndented("pass") + } } -private fun buildConstructor(`class`: PythonClass) = buildString { - appendLine("def __init__(${buildParameters(`class`.constructor?.parameters.orEmpty())}):") - - appendIndented(4) { - val attributes = buildAttributeAssignments(`class`).joinToString("\n") - if (attributes.isNotBlank()) { - append("$attributes\n\n") - } - - val constructorCall = `class`.constructor?.buildConstructorCall() ?: "" - if (constructorCall.isNotBlank()) { - append(constructorCall) - } - - if (attributes.isBlank() && constructorCall.isBlank()) { +internal fun PythonEnum.toPythonCode() = buildString { + appendLine("class $name(Enum):") + appendIndented { + if (instances.isEmpty()) { append("pass") + } else { + instances.forEach { + append(it.toPythonCode()) + if (it != instances.last()) { + appendLine(",") + } + } } } } -private fun PythonConstructor.buildConstructorCall(): String { - return "self.instance = ${callToOriginalAPI!!.toPythonCode()}" +internal fun PythonEnumInstance.toPythonCode(): String { + return "$name = ${value!!.toPythonCode()}" } -/** - * Builds a string containing the formatted function content - * @receiver The function whose adapter content should be built - * @return The string containing the formatted function content - */ -fun PythonFunction.toPythonCode(): String { - val function = """ - |def $name(${buildParameters(this.parameters)}): - |${(buildFunctionBody(this)).prependIndent(" ")} - """.trimMargin() - - return when { - isStaticMethod() -> "@staticmethod\n$function" - else -> function - } -} +internal fun PythonFunction.toPythonCode() = buildString { + val parametersString = parameters.toPythonCode() + val boundariesString = parameters + .mapNotNull { it.boundary?.toPythonCode(it.name) } + .joinToString("\n") + val callString = callToOriginalAPI + ?.let { "return ${it.toPythonCode()}" } + ?: "" -private fun buildAttributeAssignments(pythonClass: PythonClass): List { - return pythonClass.attributes.map { - "self.${it.name} = ${it.value}" + if (isStaticMethod()) { + appendLine("@staticmethod") } -} - -private fun buildParameters(parameters: List): String { - var formattedFunctionParameters = "" - val implicitParameters: MutableList = ArrayList() - val positionOnlyParameters: MutableList = ArrayList() - val positionOrNameParameters: MutableList = ArrayList() - val nameOnlyParameters: MutableList = ArrayList() - parameters.forEach { pythonParameter: PythonParameter -> - when (pythonParameter.assignedBy) { - IMPLICIT -> implicitParameters.add(pythonParameter.toPythonCode()) - POSITION_ONLY -> positionOnlyParameters.add(pythonParameter.toPythonCode()) - POSITION_OR_NAME -> positionOrNameParameters.add(pythonParameter.toPythonCode()) - NAME_ONLY -> nameOnlyParameters.add(pythonParameter.toPythonCode()) + appendLine("def $name($parametersString):") + if (boundariesString.isNotBlank()) { + appendIndented(boundariesString) + if (callString.isNotBlank()) { + append("\n\n") } } - assert(implicitParameters.size < 2) - val hasImplicitParameter = implicitParameters.isNotEmpty() - val hasPositionOnlyParameters = positionOnlyParameters.isNotEmpty() - val hasPositionOrNameParameters = positionOrNameParameters.isNotEmpty() - val hasNameOnlyParameters = nameOnlyParameters.isNotEmpty() - - if (hasImplicitParameter) { - formattedFunctionParameters += implicitParameters[0] - if (hasPositionOnlyParameters || hasPositionOrNameParameters || hasNameOnlyParameters) { - formattedFunctionParameters += ", " - } + if (callString.isNotBlank()) { + appendIndented(callString) } - if (hasPositionOnlyParameters) { - formattedFunctionParameters += positionOnlyParameters.joinToString() - formattedFunctionParameters += when { - hasPositionOrNameParameters -> ", /, " - hasNameOnlyParameters -> ", /" - else -> ", /" - } + if (boundariesString.isBlank() && callString.isBlank()) { + appendIndented("pass") } - if (hasPositionOrNameParameters) { - formattedFunctionParameters += positionOrNameParameters.joinToString() - } - if (hasNameOnlyParameters) { - formattedFunctionParameters += when { - hasPositionOnlyParameters || hasPositionOrNameParameters -> ", *, " - else -> "*, " - } - formattedFunctionParameters += nameOnlyParameters.joinToString() - } - return formattedFunctionParameters } -private fun PythonParameter.toPythonCode() = buildString { - append(name) - if (defaultValue != null) { - append("=$defaultValue") - } -} +internal fun List.toPythonCode(): String { + val assignedByToParameter = this@toPythonCode.groupBy { it.assignedBy } + val implicitParametersString = assignedByToParameter[IMPLICIT] + ?.joinToString { it.toPythonCode() } + ?: "" + var positionOnlyParametersString = assignedByToParameter[POSITION_ONLY] + ?.joinToString { it.toPythonCode() } + ?: "" + val positionOrNameParametersString = assignedByToParameter[POSITION_OR_NAME] + ?.joinToString { it.toPythonCode() } + ?: "" + var nameOnlyParametersString = assignedByToParameter[NAME_ONLY] + ?.joinToString { it.toPythonCode() } + ?: "" -private fun buildFunctionBody(pythonFunction: PythonFunction): String { - var formattedBoundaries = buildBoundaryChecks(pythonFunction).joinToString("\n".repeat(1)) - if (formattedBoundaries.isNotBlank()) { - formattedBoundaries = "$formattedBoundaries\n" + if (positionOnlyParametersString.isNotBlank()) { + positionOnlyParametersString = "$positionOnlyParametersString, /" } - if (!pythonFunction.isMethod() || pythonFunction.isStaticMethod()) { - return ( - formattedBoundaries + - pythonFunction.callToOriginalAPI!!.toPythonCode() - ) + if (nameOnlyParametersString.isNotBlank()) { + nameOnlyParametersString = "*, $nameOnlyParametersString" } - return ( - formattedBoundaries + - pythonFunction.callToOriginalAPI!!.toPythonCode() - ) + val parameterStrings = listOf( + implicitParametersString, + positionOnlyParametersString, + positionOrNameParametersString, + nameOnlyParametersString + ) + + return parameterStrings + .filter { it.isNotBlank() } + .joinToString() } -private fun buildBoundaryChecks(pythonFunction: PythonFunction): List { - val formattedBoundaries: MutableList = ArrayList() - pythonFunction - .parameters - .filter { it.boundary != null } - .forEach { - assert(it.boundary != null) - if (it.boundary!!.isDiscrete) { - formattedBoundaries.add( - "if not (isinstance(${it.name}, int) or (isinstance(${it.name}, float) and ${it.name}.is_integer())):\n" + - " raise ValueError('" + - it.name + - " needs to be an integer, but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } - if (it.boundary!!.lowerLimitType !== ComparisonOperator.UNRESTRICTED && it.boundary!!.upperLimitType !== ComparisonOperator.UNRESTRICTED) { - formattedBoundaries.add( - "if not ${it.boundary!!.lowerIntervalLimit} ${it.boundary!!.lowerLimitType.operator} ${it.name} ${it.boundary!!.upperLimitType.operator} ${it.boundary!!.upperIntervalLimit}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be in " + - it.boundary!!.asInterval() + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } else if (it.boundary!!.lowerLimitType === ComparisonOperator.LESS_THAN) { - formattedBoundaries.add( - "if not ${it.boundary!!.lowerIntervalLimit} < ${it.name}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be greater than " + - it.boundary!!.lowerIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } else if (it.boundary!!.lowerLimitType === ComparisonOperator.LESS_THAN_OR_EQUALS) { - formattedBoundaries.add( - "if not ${it.boundary!!.lowerIntervalLimit} <= ${it.name}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be greater than or equal to " + - it.boundary!!.lowerIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } - if (it.boundary!!.upperLimitType === ComparisonOperator.LESS_THAN) { - formattedBoundaries.add( - "if not ${it.name} < ${it.boundary!!.upperIntervalLimit}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be less than " + - it.boundary!!.upperIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } else if (it.boundary!!.upperLimitType === ComparisonOperator.LESS_THAN) { - formattedBoundaries.add( - "if not ${it.name} <= ${it.boundary!!.upperIntervalLimit}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be less than or equal to " + - it.boundary!!.upperIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } - } - return formattedBoundaries +internal fun PythonParameter.toPythonCode() = buildString { + val typeStringOrNull = type.toPythonCodeOrNull() + + append(name) + if (typeStringOrNull != null) { + append(": $typeStringOrNull") + defaultValue?.toPythonCode()?.let { append(" = $it") } + } else { + defaultValue?.toPythonCode()?.let { append("=$it") } + } } -private fun PythonExpression.toPythonCode(): String { +/* ******************************************************************************************************************** + * Expressions + * ********************************************************************************************************************/ + +internal fun PythonExpression.toPythonCode(): String { return when (this) { - is PythonBoolean -> value.toString() - is PythonCall -> "$receiver(${arguments.joinToString { it.toPythonCode() }})" + is PythonBoolean -> value.toString().replaceFirstChar { it.uppercase() } + is PythonCall -> "${receiver!!.toPythonCode()}(${arguments.joinToString { it.toPythonCode() }})" is PythonFloat -> value.toString() is PythonInt -> value.toString() - is PythonString -> "'$value'" is PythonMemberAccess -> "${receiver!!.toPythonCode()}.${member!!.toPythonCode()}" is PythonReference -> declaration!!.name + is PythonString -> "'$value'" + is PythonStringifiedExpression -> string } } -private fun PythonArgument.toPythonCode() = buildString { +/* ******************************************************************************************************************** + * Types + * ********************************************************************************************************************/ + +internal fun PythonType?.toPythonCodeOrNull(): String? { + return when (this) { + is PythonNamedType -> this.declaration?.name + is PythonStringifiedType -> { + when (this.string) { + "bool" -> "bool" + "float" -> "float" + "int" -> "int" + "str" -> "str" + else -> null + } + } + null -> null + } +} + +/* ******************************************************************************************************************** + * Other + * ********************************************************************************************************************/ + +internal fun PythonArgument.toPythonCode() = buildString { if (name != null) { append("$name=") } append(value!!.toPythonCode()) } -internal fun PythonEnum.toPythonCode() = buildString { - appendLine("class $name(Enum):") - appendIndented(4) { - if (instances.isEmpty()) { - append("pass") - } else { - instances.forEach { - append("${it.name} = \"${it.value}\"") - if (it != instances.last()) { - appendLine(",") - } - } +internal fun Boundary.toPythonCode(parameterName: String) = buildString { + if (isDiscrete) { + appendLine("if not (isinstance($parameterName, int) or (isinstance($parameterName, float) and $parameterName.is_integer())):") + appendIndented("raise ValueError(f'$parameterName needs to be an integer, but {$parameterName} was assigned.')") + if (lowerLimitType != UNRESTRICTED || upperLimitType != UNRESTRICTED) { + appendLine() } } + + if (lowerLimitType != UNRESTRICTED && upperLimitType != UNRESTRICTED) { + appendLine("if not $lowerIntervalLimit ${lowerLimitType.operator} $parameterName ${upperLimitType.operator} $upperIntervalLimit:") + appendIndented("raise ValueError(f'Valid values of $parameterName must be in ${asInterval()}, but {$parameterName} was assigned.')") + } else if (lowerLimitType == LESS_THAN) { + appendLine("if not $lowerIntervalLimit < $parameterName:") + appendIndented("raise ValueError(f'Valid values of $parameterName must be greater than $lowerIntervalLimit, but {$parameterName} was assigned.')") + } else if (lowerLimitType == LESS_THAN_OR_EQUALS) { + appendLine("if not $lowerIntervalLimit <= $parameterName:") + appendIndented("raise ValueError(f'Valid values of $parameterName must be greater than or equal to $lowerIntervalLimit, but {$parameterName} was assigned.')") + } else if (upperLimitType == LESS_THAN) { + appendLine("if not $parameterName < $upperIntervalLimit:") + appendIndented("raise ValueError(f'Valid values of $parameterName must be less than $upperIntervalLimit, but {$parameterName} was assigned.')") + } else if (upperLimitType == LESS_THAN_OR_EQUALS) { + appendLine("if not $parameterName <= $upperIntervalLimit:") + appendIndented("raise ValueError(f'Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {$parameterName} was assigned.')") + } } -private fun StringBuilder.appendIndented(numberOfSpaces: Int, init: StringBuilder.() -> Unit): StringBuilder { +/* ******************************************************************************************************************** + * Util + * ********************************************************************************************************************/ + +private fun String.prependIndentUnlessBlank(indent: String = " "): String { + return lineSequence() + .map { + when { + it.isBlank() -> it.trim() + else -> it.prependIndent(indent) + } + } + .joinToString("\n") +} + +private fun StringBuilder.appendIndented(init: StringBuilder.() -> Unit): StringBuilder { val stringToIndent = StringBuilder().apply(init).toString() - val indent = " ".repeat(numberOfSpaces) - append(stringToIndent.prependIndent(indent)) + append(stringToIndent.prependIndentUnlessBlank()) + return this +} + +private fun StringBuilder.appendIndented(value: String): StringBuilder { + append(value.prependIndentUnlessBlank()) return this } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt index ee1e38934..2dcd2f247 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt @@ -5,10 +5,15 @@ import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance +import com.larsreimann.api_editor.mutable_model.PythonExpression import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType +import com.larsreimann.api_editor.mutable_model.PythonType import de.unibonn.simpleml.constant.SmlFileExtension import de.unibonn.simpleml.emf.createSmlAnnotationUse import de.unibonn.simpleml.emf.createSmlArgument @@ -124,7 +129,7 @@ internal fun PythonAttribute.toSmlAttribute(): SmlAttribute { add(createSmlDescriptionAnnotationUse(description)) } }, - type = typeInDocs.toSmlType() + type = type.toSmlType() ) } @@ -181,7 +186,7 @@ internal fun PythonParameter.toSmlParameterOrNull(): SmlParameter? { add(createSmlDescriptionAnnotationUse(description)) } }, - type = typeInDocs.toSmlType(), + type = type.toSmlType(), defaultValue = defaultValue?.toSmlExpression() ) } @@ -258,21 +263,34 @@ private fun String.snakeCaseToCamelCase(): String { // Type conversions ---------------------------------------------------------------------------------------------------- -internal fun String.toSmlType(): SmlAbstractType { +internal fun PythonType?.toSmlType(): SmlAbstractType { return when (this) { - "bool" -> createSmlNamedType( - declaration = createSmlClass("Boolean") - ) - "float" -> createSmlNamedType( - declaration = createSmlClass("Float") - ) - "int" -> createSmlNamedType( - declaration = createSmlClass("Int") - ) - "str" -> createSmlNamedType( - declaration = createSmlClass("String") - ) - else -> createSmlNamedType( + is PythonNamedType -> { + createSmlNamedType( + declaration = createSmlClass(this.declaration!!.name) + ) + } + is PythonStringifiedType -> { + when (this.string) { + "bool" -> createSmlNamedType( + declaration = createSmlClass("Boolean") + ) + "float" -> createSmlNamedType( + declaration = createSmlClass("Float") + ) + "int" -> createSmlNamedType( + declaration = createSmlClass("Int") + ) + "str" -> createSmlNamedType( + declaration = createSmlClass("String") + ) + else -> createSmlNamedType( + declaration = createSmlClass("Any"), + isNullable = true + ) + } + } + null -> createSmlNamedType( declaration = createSmlClass("Any"), isNullable = true ) @@ -281,16 +299,20 @@ internal fun String.toSmlType(): SmlAbstractType { // Value conversions --------------------------------------------------------------------------------------------------- -internal fun String.toSmlExpression(): SmlAbstractExpression? { +internal fun PythonExpression.toSmlExpression(): SmlAbstractExpression? { + if (this !is PythonStringifiedExpression) { + return createSmlString("###invalid###$this###") + } + return when { - isBlank() -> null - this == "False" -> createSmlBoolean(false) - this == "True" -> createSmlBoolean(true) - this == "None" -> createSmlNull() - isIntLiteral() -> createSmlInt(toInt()) - isFloatLiteral() -> createSmlFloat(toDouble()) - isStringLiteral() -> createSmlString(substring(1, length - 1)) - else -> createSmlString("###invalid###$this###") + string.isBlank() -> null + string == "False" -> createSmlBoolean(false) + string == "True" -> createSmlBoolean(true) + string == "None" -> createSmlNull() + string.isIntLiteral() -> createSmlInt(string.toInt()) + string.isFloatLiteral() -> createSmlFloat(string.toDouble()) + string.isStringLiteral() -> createSmlString(string.substring(1, string.length - 1)) + else -> createSmlString("###invalid###$string###") } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index 681807a0d..9aa59f7af 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -71,7 +71,7 @@ data class EnumAnnotation(val enumName: String, val pairs: List) : Edi data class EnumPair(val stringValue: String, val instanceName: String) @Serializable -data class GroupAnnotation(val groupName: String, val parameters: List) : EditorAnnotation() { +data class GroupAnnotation(val groupName: String, val parameters: MutableList) : EditorAnnotation() { @Transient override val validTargets = FUNCTIONS @@ -129,14 +129,14 @@ sealed class DefaultValue @Serializable class DefaultBoolean(val value: Boolean) : DefaultValue() { override fun toString(): String { - return "$value" + return value.toString().replaceFirstChar { it.uppercase() } } } @Serializable class DefaultNumber(val value: Double) : DefaultValue() { override fun toString(): String { - return "$value" + return value.toString() } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt deleted file mode 100644 index 2a0ea04ae..000000000 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.larsreimann.api_editor.mutable_model - -import com.larsreimann.api_editor.model.SerializablePythonAttribute -import com.larsreimann.api_editor.model.SerializablePythonClass -import com.larsreimann.api_editor.model.SerializablePythonEnum -import com.larsreimann.api_editor.model.SerializablePythonEnumInstance -import com.larsreimann.api_editor.model.SerializablePythonFunction -import com.larsreimann.api_editor.model.SerializablePythonModule -import com.larsreimann.api_editor.model.SerializablePythonPackage -import com.larsreimann.api_editor.model.SerializablePythonParameter -import com.larsreimann.api_editor.model.SerializablePythonResult - -fun convertPackage(pythonPackage: PythonPackage): SerializablePythonPackage { - return SerializablePythonPackage( - distribution = pythonPackage.distribution, - name = pythonPackage.name, - version = pythonPackage.version, - modules = pythonPackage.modules.map { convertModule(it) }.toMutableList(), - annotations = pythonPackage.annotations - ) -} - -fun convertModule(pythonModule: PythonModule): SerializablePythonModule { - val result = SerializablePythonModule( - name = pythonModule.name, - imports = pythonModule.imports, - fromImports = pythonModule.fromImports, - classes = pythonModule.classes.map { convertClass(it) }.toMutableList(), - functions = pythonModule.functions.map { convertFunction(it) }.toMutableList(), - annotations = pythonModule.annotations - ) - result.enums += pythonModule.enums.map { convertEnum(it) } - return result -} - -fun convertClass(pythonClass: PythonClass): SerializablePythonClass { - val result = SerializablePythonClass( - name = pythonClass.name, - qualifiedName = pythonClass.qualifiedName(), - decorators = pythonClass.decorators, - superclasses = pythonClass.superclasses, - methods = pythonClass.methods.map { convertFunction(it) }.toMutableList(), - isPublic = pythonClass.isPublic, - description = pythonClass.description, - fullDocstring = pythonClass.fullDocstring, - annotations = pythonClass.annotations - ) - result.attributes += pythonClass.attributes.map { convertAttribute(it) } - return result -} - -fun convertEnum(pythonEnum: PythonEnum): SerializablePythonEnum { - return SerializablePythonEnum( - name = pythonEnum.name, - instances = pythonEnum.instances.map { convertEnumInstance(it) }.toMutableList(), - annotations = pythonEnum.annotations - ) -} - -fun convertEnumInstance(pythonEnumInstance: PythonEnumInstance): SerializablePythonEnumInstance { - val instance = SerializablePythonEnumInstance( - name = pythonEnumInstance.name, - value = pythonEnumInstance.value - ) - instance.description = pythonEnumInstance.description - return instance -} - -fun convertFunction(pythonFunction: PythonFunction): SerializablePythonFunction { - val result = SerializablePythonFunction( - name = pythonFunction.name, - qualifiedName = pythonFunction.qualifiedName(), - decorators = pythonFunction.decorators, - parameters = pythonFunction.parameters.map { convertParameter(it) }.toMutableList(), - results = pythonFunction.results.map { convertResult(it) }.toMutableList(), - isPublic = pythonFunction.isPublic, - description = pythonFunction.description, - fullDocstring = pythonFunction.fullDocstring, - annotations = pythonFunction.annotations, - ) - result.calledAfter += pythonFunction.calledAfter - result.isPure = pythonFunction.isPure - return result -} - -fun convertAttribute(pythonAttribute: PythonAttribute): SerializablePythonAttribute { - val result = SerializablePythonAttribute( - name = pythonAttribute.name, - qualifiedName = pythonAttribute.qualifiedName(), - defaultValue = pythonAttribute.value, - isPublic = pythonAttribute.isPublic, - typeInDocs = pythonAttribute.typeInDocs, - description = pythonAttribute.description, - annotations = pythonAttribute.annotations - ) - result.boundary = pythonAttribute.boundary - return result -} - -fun convertParameter(pythonParameter: PythonParameter): SerializablePythonParameter { - val result = SerializablePythonParameter( - name = pythonParameter.name, - qualifiedName = pythonParameter.qualifiedName(), - defaultValue = pythonParameter.defaultValue, - assignedBy = pythonParameter.assignedBy, - isPublic = true, - typeInDocs = pythonParameter.typeInDocs, - description = pythonParameter.description, - annotations = pythonParameter.annotations - ) - result.boundary = pythonParameter.boundary - return result -} - -fun convertResult(pythonResult: PythonResult): SerializablePythonResult { - val result = SerializablePythonResult( - name = pythonResult.name, - type = pythonResult.type, - typeInDocs = pythonResult.typeInDocs, - description = pythonResult.description, - annotations = pythonResult.annotations - ) - result.boundary = pythonResult.boundary - return result -} diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt index b5f13571f..cf2980c18 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt @@ -36,12 +36,11 @@ fun convertClass(pythonClass: SerializablePythonClass): PythonClass { return PythonClass( name = pythonClass.name, decorators = pythonClass.decorators.toMutableList(), - superclasses = pythonClass.superclasses.toMutableList(), + superclasses = pythonClass.superclasses.map { PythonNamedType(PythonClass(name = it)) }.toMutableList(), attributes = pythonClass.attributes.map { convertAttribute(it) }, methods = pythonClass.methods.map { convertFunction(it) }, isPublic = pythonClass.isPublic, description = pythonClass.description, - fullDocstring = pythonClass.fullDocstring, annotations = pythonClass.annotations ) } @@ -57,7 +56,7 @@ fun convertEnum(pythonEnum: SerializablePythonEnum): PythonEnum { fun convertEnumInstance(pythonEnumInstance: SerializablePythonEnumInstance): PythonEnumInstance { return PythonEnumInstance( name = pythonEnumInstance.name, - value = pythonEnumInstance.value, + value = PythonStringifiedExpression(pythonEnumInstance.value), description = pythonEnumInstance.description, annotations = mutableListOf() ) @@ -71,8 +70,6 @@ fun convertFunction(pythonFunction: SerializablePythonFunction): PythonFunction results = pythonFunction.results.map { convertResult(it) }, isPublic = pythonFunction.isPublic, description = pythonFunction.description, - fullDocstring = pythonFunction.fullDocstring, -// calledAfter = pythonFunction.calledAfter, isPure = pythonFunction.isPure, annotations = pythonFunction.annotations, ) @@ -81,9 +78,9 @@ fun convertFunction(pythonFunction: SerializablePythonFunction): PythonFunction fun convertAttribute(pythonAttribute: SerializablePythonAttribute): PythonAttribute { return PythonAttribute( name = pythonAttribute.name, - value = pythonAttribute.defaultValue, + type = PythonStringifiedType(pythonAttribute.typeInDocs), + value = pythonAttribute.defaultValue?.let { PythonStringifiedExpression(it) }, isPublic = pythonAttribute.isPublic, - typeInDocs = pythonAttribute.typeInDocs, description = pythonAttribute.description, boundary = pythonAttribute.boundary, annotations = pythonAttribute.annotations, @@ -93,9 +90,9 @@ fun convertAttribute(pythonAttribute: SerializablePythonAttribute): PythonAttrib fun convertParameter(pythonParameter: SerializablePythonParameter): PythonParameter { return PythonParameter( name = pythonParameter.name, - defaultValue = pythonParameter.defaultValue, + type = PythonStringifiedType(pythonParameter.typeInDocs), + defaultValue = pythonParameter.defaultValue?.let { PythonStringifiedExpression(it) }, assignedBy = pythonParameter.assignedBy, - typeInDocs = pythonParameter.typeInDocs, description = pythonParameter.description, boundary = pythonParameter.boundary, annotations = pythonParameter.annotations, @@ -105,8 +102,7 @@ fun convertParameter(pythonParameter: SerializablePythonParameter): PythonParame fun convertResult(pythonResult: SerializablePythonResult): PythonResult { return PythonResult( name = pythonResult.name, - type = pythonResult.type, - typeInDocs = pythonResult.typeInDocs, + type = PythonStringifiedType(pythonResult.type), description = pythonResult.description, boundary = pythonResult.boundary, annotations = pythonResult.annotations, diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index e2711a964..2930625b0 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -7,7 +7,6 @@ import com.larsreimann.api_editor.model.EditorAnnotation import com.larsreimann.api_editor.model.PythonFromImport import com.larsreimann.api_editor.model.PythonImport import com.larsreimann.api_editor.model.PythonParameterAssignment -import com.larsreimann.api_editor.model.SerializablePythonFunction import com.larsreimann.modeling.ModelNode import com.larsreimann.modeling.ancestorsOrSelf @@ -35,61 +34,46 @@ sealed class PythonDeclaration : PythonAstNode() { } } -class PythonPackage( - var distribution: String, - override var name: String, - var version: String, - modules: List = emptyList(), - override val annotations: MutableList = mutableListOf() -) : PythonDeclaration() { - - val modules = MutableContainmentList(modules) - - override fun children() = sequence { - yieldAll(modules) - } -} - -class PythonModule( +class PythonAttribute( override var name: String, - val imports: MutableList = mutableListOf(), - val fromImports: MutableList = mutableListOf(), - classes: List = emptyList(), - enums: List = emptyList(), - functions: List = emptyList(), - override val annotations: MutableList = mutableListOf() + type: PythonType? = null, + value: PythonExpression? = null, + var isPublic: Boolean = true, + var description: String = "", + var boundary: Boundary? = null, + override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { - val classes = MutableContainmentList(classes) - val enums = MutableContainmentList(enums) - val functions = MutableContainmentList(functions) + var type by ContainmentReference(type) + var value by ContainmentReference(value) override fun children() = sequence { - yieldAll(classes) - yieldAll(enums) - yieldAll(functions) + type?.let { yield(it) } + value?.let { yield(it) } } } class PythonClass( override var name: String, val decorators: MutableList = mutableListOf(), - val superclasses: MutableList = mutableListOf(), + superclasses: MutableList = mutableListOf(), constructor: PythonConstructor? = null, attributes: List = emptyList(), methods: List = emptyList(), var isPublic: Boolean = true, var description: String = "", - var fullDocstring: String = "", override val annotations: MutableList = mutableListOf(), var originalClass: OriginalPythonClass? = null ) : PythonDeclaration() { + val superclasses = MutableContainmentList(superclasses) var constructor by ContainmentReference(constructor) val attributes = MutableContainmentList(attributes) val methods = MutableContainmentList(methods) override fun children() = sequence { + yieldAll(superclasses) + constructor?.let { yield(it) } yieldAll(attributes) yieldAll(methods) } @@ -107,8 +91,6 @@ class PythonConstructor( } } -data class OriginalPythonClass(val qualifiedName: String) - class PythonEnum( override var name: String, instances: List = emptyList(), @@ -117,14 +99,25 @@ class PythonEnum( ) : PythonDeclaration() { val instances = MutableContainmentList(instances) + + override fun children() = sequence { + yieldAll(instances) + } } -data class PythonEnumInstance( +class PythonEnumInstance( override var name: String, - val value: String = name, + value: PythonExpression = PythonString(name), var description: String = "", override val annotations: MutableList = mutableListOf() -) : PythonDeclaration() +) : PythonDeclaration() { + + var value by ContainmentReference(value) + + override fun children() = sequence { + value?.let { yield(it) } + } +} class PythonFunction( override var name: String, @@ -133,10 +126,8 @@ class PythonFunction( results: List = emptyList(), var isPublic: Boolean = true, var description: String = "", - var fullDocstring: String = "", var isPure: Boolean = false, override val annotations: MutableList = mutableListOf(), - val calledAfter: MutableList = mutableListOf(), var callToOriginalAPI: PythonCall? = null ) : PythonDeclaration() { @@ -152,39 +143,81 @@ class PythonFunction( fun isStaticMethod() = isMethod() && "staticmethod" in decorators } -data class PythonAttribute( +class PythonModule( override var name: String, - var value: String? = null, - var isPublic: Boolean = true, - var typeInDocs: String = "", - var description: String = "", - var boundary: Boundary? = null, - override val annotations: MutableList = mutableListOf(), -) : PythonDeclaration() + val imports: MutableList = mutableListOf(), + val fromImports: MutableList = mutableListOf(), + classes: List = emptyList(), + enums: List = emptyList(), + functions: List = emptyList(), + override val annotations: MutableList = mutableListOf() +) : PythonDeclaration() { + + val classes = MutableContainmentList(classes) + val enums = MutableContainmentList(enums) + val functions = MutableContainmentList(functions) -data class PythonParameter( + override fun children() = sequence { + yieldAll(classes) + yieldAll(enums) + yieldAll(functions) + } +} + +class PythonPackage( + var distribution: String, + override var name: String, + var version: String, + modules: List = emptyList(), + override val annotations: MutableList = mutableListOf() +) : PythonDeclaration() { + + val modules = MutableContainmentList(modules) + + override fun children() = sequence { + yieldAll(modules) + } +} + +class PythonParameter( override var name: String, - var defaultValue: String? = null, + type: PythonType? = null, + defaultValue: PythonExpression? = null, var assignedBy: PythonParameterAssignment = PythonParameterAssignment.POSITION_OR_NAME, - var typeInDocs: String = "", var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { + var type by ContainmentReference(type) + var defaultValue by ContainmentReference(defaultValue) + + override fun children() = sequence { + type?.let { yield(it) } + defaultValue?.let { yield(it) } + } + fun isRequired() = defaultValue == null fun isOptional() = defaultValue != null } -data class PythonResult( +class PythonResult( override var name: String, - var type: String = "", - var typeInDocs: String = "", + type: PythonType? = null, var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), -) : PythonDeclaration() +) : PythonDeclaration() { + + var type by ContainmentReference(type) + + override fun children() = sequence { + type?.let { yield(it) } + } +} + +data class OriginalPythonClass(val qualifiedName: String) /* ******************************************************************************************************************** * Expressions @@ -192,21 +225,26 @@ data class PythonResult( sealed class PythonExpression : PythonAstNode() -class PythonCall(val receiver: String, arguments: List = emptyList()) : PythonExpression() { +class PythonCall( + receiver: PythonExpression, + arguments: List = emptyList() +) : PythonExpression() { + + var receiver by ContainmentReference(receiver) val arguments = MutableContainmentList(arguments) override fun children() = sequence { + receiver?.let { yield(it) } yieldAll(arguments) } } -class PythonArgument(val name: String? = null, value: PythonExpression) : PythonAstNode() { - var value by ContainmentReference(value) +sealed class PythonLiteral : PythonExpression() - override fun children() = sequence { - value?.let { yield(it) } - } -} +data class PythonBoolean(val value: Boolean) : PythonLiteral() +data class PythonFloat(val value: Double) : PythonLiteral() +data class PythonInt(val value: Int) : PythonLiteral() +data class PythonString(val value: String) : PythonLiteral() class PythonMemberAccess( receiver: PythonExpression, @@ -226,9 +264,38 @@ class PythonReference(declaration: PythonDeclaration) : PythonExpression() { var declaration by CrossReference(declaration) } -sealed class PythonLiteral : PythonExpression() +data class PythonStringifiedExpression(val string: String) : PythonExpression() -data class PythonBoolean(val value: Boolean) : PythonLiteral() -data class PythonFloat(val value: Double) : PythonLiteral() -data class PythonInt(val value: Int) : PythonLiteral() -data class PythonString(val value: String) : PythonLiteral() +/* ******************************************************************************************************************** + * Types + * ********************************************************************************************************************/ + +sealed class PythonType : PythonAstNode() { + abstract fun copy(): PythonType +} + +class PythonNamedType(declaration: PythonDeclaration?) : PythonType() { + var declaration by CrossReference(declaration) + + override fun copy(): PythonNamedType { + return PythonNamedType(declaration) + } +} + +data class PythonStringifiedType(val string: String) : PythonType() { + override fun copy(): PythonStringifiedType { + return PythonStringifiedType(string) + } +} + +/* ******************************************************************************************************************** + * Other + * ********************************************************************************************************************/ + +class PythonArgument(val name: String? = null, value: PythonExpression) : PythonAstNode() { + var value by ContainmentReference(value) + + override fun children() = sequence { + value?.let { yield(it) } + } +} diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt index 290764ef1..2aa52edb7 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt @@ -7,9 +7,11 @@ import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonString import com.larsreimann.api_editor.transformation.processing_exceptions.ConflictingEnumException import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants @@ -38,7 +40,7 @@ private fun PythonParameter.processEnumAnnotations(module: PythonModule) { annotation.pairs.map { enumPair -> PythonEnumInstance( enumPair.instanceName, - enumPair.stringValue + PythonString(enumPair.stringValue) ) } ) @@ -68,7 +70,7 @@ private fun PythonParameter.processEnumAnnotations(module: PythonModule) { member = PythonReference(PythonAttribute(name = "value")) ) - this.typeInDocs = annotation.enumName + this.type = PythonNamedType(enumToAdd) this.annotations -= annotation } } @@ -77,11 +79,12 @@ private fun hasConflictingEnums( moduleEnums: List, enumToCheck: PythonEnum ): Boolean { - return moduleEnums.any { - (enumToCheck.name == it.name) && + return moduleEnums.any { enum -> + (enumToCheck.name == enum.name) && ( - enumToCheck.instances.size != it.instances.size || - !enumToCheck.instances.containsAll(it.instances) + enumToCheck.instances.size != enum.instances.size || + !enumToCheck.instances.mapNotNull { (it.value as? PythonString)?.value } + .containsAll(enum.instances.mapNotNull { (it.value as? PythonString)?.value }) ) } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt index 5b6265149..686aa5b43 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt @@ -8,6 +8,7 @@ import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference @@ -26,6 +27,7 @@ fun PythonPackage.processGroupAnnotations() { private fun PythonModule.processGroupAnnotations() { this.descendants() .filterIsInstance() + .toList() .forEach { it.processGroupAnnotations(this) } } @@ -34,10 +36,8 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { .filterIsInstance() .forEach { annotation -> val firstOccurrence = this.parameters.indexOfFirst { it.name in annotation.parameters } - val groupedParameter = PythonParameter( - name = annotation.groupName.replaceFirstChar { it.lowercase() }, - typeInDocs = annotation.groupName.replaceFirstChar { it.uppercase() }, - ) + + // Create class val constructorParameters = mutableListOf( PythonParameter( name = "self", @@ -45,14 +45,21 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { ) ) constructorParameters += this.parameters.filter { it.name in annotation.parameters } - this.parameters.removeIf { it.name in annotation.parameters } - this.parameters.add(firstOccurrence, groupedParameter) val groupedParameterClass = PythonClass( name = annotation.groupName.replaceFirstChar { it.uppercase() }, constructor = PythonConstructor( parameters = constructorParameters ) ) + + // Update parameters + val groupedParameter = PythonParameter( + name = annotation.groupName.replaceFirstChar { it.lowercase() }, + type = PythonNamedType(groupedParameterClass) + ) + this.parameters.removeIf { it.name in annotation.parameters } + this.parameters.add(firstOccurrence, groupedParameter) + if (hasConflictingGroups(module.classes, groupedParameterClass)) { throw ConflictingGroupException( groupedParameterClass.name, @@ -69,13 +76,30 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { val value = it.value if (value is PythonReference && value.declaration?.name in annotation.parameters) { it.value = PythonMemberAccess( - receiver = PythonReference(declaration = groupedParameterClass), + receiver = PythonReference(declaration = groupedParameter), member = PythonReference(PythonAttribute(name = value.declaration!!.name)) ) + } else if (value is PythonMemberAccess) { + val receiver = value.receiver + val member = value.member + if (receiver is PythonReference && member is PythonReference) { + val receiverMatches = receiver.declaration?.name in annotation.parameters + val memberMatches = member.declaration is PythonAttribute && member.declaration?.name == "value" + + if (receiverMatches && memberMatches) { + it.value = PythonMemberAccess( + receiver = PythonMemberAccess( + receiver = PythonReference(declaration = groupedParameter), + member = receiver + ), + member = member + ) + } + } } - } - this.annotations.remove(annotation) + this.annotations.remove(annotation) + } } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt index d980e0984..d09f3f8ab 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt @@ -20,6 +20,7 @@ import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants @@ -53,9 +54,9 @@ private fun PythonParameter.processAttributeAnnotation(annotation: AttributeAnno val containingClass = this.closest()!! containingClass.attributes += PythonAttribute( name = name, - value = annotation.defaultValue.toString(), + type = type, + value = PythonStringifiedExpression(annotation.defaultValue.toString()), isPublic = true, - typeInDocs = typeInDocs, description = description, boundary = boundary ) @@ -96,7 +97,7 @@ private fun PythonParameter.processConstantAnnotation(annotation: ConstantAnnota private fun PythonParameter.processOptionalAnnotation(annotation: OptionalAnnotation) { this.assignedBy = PythonParameterAssignment.NAME_ONLY - this.defaultValue = annotation.defaultValue.toString() + this.defaultValue = PythonStringifiedExpression(annotation.defaultValue.toString()) this.annotations.remove(annotation) } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 454138859..940389321 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -7,7 +7,11 @@ import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.modeling.ModelNode import com.larsreimann.modeling.descendants +import java.lang.IllegalStateException /** * Removes modules that don't contain declarations. @@ -27,16 +31,20 @@ fun PythonPackage.removeEmptyModules() { */ fun PythonPackage.reorderParameters() { this.descendants() - .filterIsInstance() - .forEach { it.reorderParameters() } + .forEach { + when (it) { + is PythonConstructor -> it.parameters.reorderParameters() + is PythonFunction -> it.parameters.reorderParameters() + } + } } -private fun PythonFunction.reorderParameters() { - val groups = this.parameters.groupBy { it.assignedBy } - this.parameters.addAll(groups[PythonParameterAssignment.IMPLICIT].orEmpty()) - this.parameters.addAll(groups[PythonParameterAssignment.POSITION_ONLY].orEmpty()) - this.parameters.addAll(groups[PythonParameterAssignment.POSITION_OR_NAME].orEmpty()) - this.parameters.addAll(groups[PythonParameterAssignment.NAME_ONLY].orEmpty()) +private fun ModelNode.MutableContainmentList.reorderParameters() { + val groups = this.groupBy { it.assignedBy } + this.addAll(groups[PythonParameterAssignment.IMPLICIT].orEmpty()) + this.addAll(groups[PythonParameterAssignment.POSITION_ONLY].orEmpty()) + this.addAll(groups[PythonParameterAssignment.POSITION_OR_NAME].orEmpty()) + this.addAll(groups[PythonParameterAssignment.NAME_ONLY].orEmpty()) } /** @@ -52,19 +60,33 @@ fun PythonPackage.extractConstructors() { private fun PythonClass.createConstructor() { when (val constructorMethod = this.methods.firstOrNull { it.name == "__init__" }) { null -> { - this.constructor = PythonConstructor( - parameters = emptyList(), - callToOriginalAPI = PythonCall(receiver = this.originalClass!!.qualifiedName) - ) + if (this.originalClass != null) { + this.constructor = PythonConstructor( + parameters = emptyList(), + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression(this.originalClass!!.qualifiedName) + ) + ) + } } else -> { - this.constructor = PythonConstructor( - parameters = constructorMethod.parameters.toList(), - callToOriginalAPI = PythonCall( - receiver = constructorMethod.callToOriginalAPI!!.receiver.removeSuffix(".__init__"), - arguments = constructorMethod.callToOriginalAPI!!.arguments.toList() + constructorMethod.callToOriginalAPI?.let { callToOriginalAPI -> + val newReceiver = when (val receiver = callToOriginalAPI.receiver) { + is PythonStringifiedExpression -> PythonStringifiedExpression( + receiver.string.removeSuffix(".__init__") + ) + null -> throw IllegalStateException("Receiver of call is null: $callToOriginalAPI") + else -> receiver + } + + this.constructor = PythonConstructor( + parameters = constructorMethod.parameters.toList(), + callToOriginalAPI = PythonCall( + receiver = newReceiver, + arguments = callToOriginalAPI.arguments.toList() + ) ) - ) + } constructorMethod.release() } @@ -87,9 +109,9 @@ private fun PythonClass.createAttributesForParametersOfConstructor() { ?.forEach { this.attributes += PythonAttribute( name = it.name, - value = it.defaultValue, + type = it.type?.copy(), + value = PythonStringifiedExpression(it.name), isPublic = true, - typeInDocs = it.typeInDocs, description = it.description, boundary = it.boundary ) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt index 66856ec67..96abf2cbb 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt @@ -10,6 +10,7 @@ import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants @@ -66,11 +67,13 @@ private fun PythonClass.addOriginalDeclarations() { private fun PythonFunction.addOriginalDeclarations() { val containingClass = closest() this.callToOriginalAPI = PythonCall( - receiver = when { - name == "__init__" && containingClass != null -> containingClass.originalClass!!.qualifiedName - isMethod() -> "self.instance.$name" - else -> qualifiedName() - }, + receiver = PythonStringifiedExpression( + when { + name == "__init__" && containingClass != null -> containingClass.originalClass!!.qualifiedName + isMethod() -> "self.instance.$name" + else -> qualifiedName() + } + ), arguments = this.parameters .filter { !it.isImplicit() } .map { diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt index 32a01f484..d049433b9 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt @@ -1,8 +1,12 @@ package com.larsreimann.api_editor.transformation +import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.mutable_model.PythonDeclaration +import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants /** @@ -18,7 +22,22 @@ private fun PythonDeclaration.processRenameAnnotations() { this.annotations .filterIsInstance() .forEach { + (this as? PythonParameter)?.updateGroupAnnotationOnContainingFunction(it.newName) this.name = it.newName this.annotations.remove(it) } } + +private fun PythonParameter.updateGroupAnnotationOnContainingFunction(newName: String) { + val containingFunction = closest() ?: return + containingFunction.annotations + .filterIsInstance() + .forEach { annotation -> + annotation.parameters.replaceAll { oldName -> + when (oldName) { + this.name -> newName + else -> oldName + } + } + } +} diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt index f474158e9..b627c91e6 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt @@ -181,14 +181,14 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython companion object { private var possibleCombinations = buildMap> { - this["Attribute"] = mutableSetOf("Boundary", "Enum", "Rename") - this["Boundary"] = mutableSetOf("Attribute", "Group", "Optional", "Rename", "Required") + this["Attribute"] = mutableSetOf("Rename") + this["Boundary"] = mutableSetOf("Group", "Optional", "Rename", "Required") this["CalledAfter"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") this["Constant"] = mutableSetOf() - this["Enum"] = mutableSetOf("Attribute", "Group", "Optional", "Rename", "Required") - this["Group"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") + this["Enum"] = mutableSetOf("Group", "Rename", "Required") + this["Group"] = mutableSetOf("Boundary", "CalledAfter", "Enum", "Group", "Move", "Optional", "Rename", "Required") this["Move"] = mutableSetOf("CalledAfter", "Group", "Rename") - this["Optional"] = mutableSetOf("Boundary", "Enum", "Group", "Rename") + this["Optional"] = mutableSetOf("Boundary", "Group", "Rename") this["Rename"] = mutableSetOf( "Attribute", "Boundary", diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt deleted file mode 100644 index cffde4d73..000000000 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ /dev/null @@ -1,1089 +0,0 @@ -package com.larsreimann.api_editor.codegen - -import com.larsreimann.api_editor.model.Boundary -import com.larsreimann.api_editor.model.ComparisonOperator -import com.larsreimann.api_editor.model.PythonFromImport -import com.larsreimann.api_editor.model.PythonImport -import com.larsreimann.api_editor.model.PythonParameterAssignment -import com.larsreimann.api_editor.mutable_model.OriginalPythonClass -import com.larsreimann.api_editor.mutable_model.PythonArgument -import com.larsreimann.api_editor.mutable_model.PythonAttribute -import com.larsreimann.api_editor.mutable_model.PythonCall -import com.larsreimann.api_editor.mutable_model.PythonClass -import com.larsreimann.api_editor.mutable_model.PythonConstructor -import com.larsreimann.api_editor.mutable_model.PythonEnum -import com.larsreimann.api_editor.mutable_model.PythonEnumInstance -import com.larsreimann.api_editor.mutable_model.PythonFunction -import com.larsreimann.api_editor.mutable_model.PythonMemberAccess -import com.larsreimann.api_editor.mutable_model.PythonModule -import com.larsreimann.api_editor.mutable_model.PythonParameter -import com.larsreimann.api_editor.mutable_model.PythonReference -import com.larsreimann.api_editor.mutable_model.PythonResult -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -class PythonCodeGeneratorTest { - @Test - fun buildModuleContentReturnsFormattedModuleContent() { // TODO - // given - val testMethodParameter = PythonParameter( - name = "only-param", - defaultValue = "'defaultValue'", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testClass = PythonClass( - name = "test-class", - methods = listOf( - PythonFunction( - name = "test-class-function", - parameters = listOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testMethodParameter - ), - callToOriginalAPI = PythonCall( - receiver = "self.instance.test-class-function", - arguments = listOf( - PythonArgument( - value = PythonReference(testMethodParameter) - ) - ) - ) - ) - ), - originalClass = OriginalPythonClass(qualifiedName = "test-module.test-class") - ) - val testFunction1Parameter1 = PythonParameter(name = "param1") - val testFunction1Parameter2 = PythonParameter(name = "param2") - val testFunction1Parameter3 = PythonParameter(name = "param3") - val testFunction2Parameter = PythonParameter( - name = "test-parameter", - defaultValue = "42", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testModule = PythonModule( - name = "test-module", - classes = mutableListOf(testClass), - functions = listOf( - PythonFunction( - name = "function_module", - parameters = listOf( - testFunction1Parameter1, - testFunction1Parameter2, - testFunction1Parameter3 - ), - results = listOf( - PythonResult( - name = "test-result", - type = "str" - ) - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testFunction1Parameter1) - ), - PythonArgument( - name = "param2", - value = PythonReference(testFunction1Parameter2) - ), - PythonArgument( - name = "param3", - value = PythonReference(testFunction1Parameter3) - ) - ) - ) - ), - PythonFunction( - name = "test-function", - parameters = listOf( - testFunction2Parameter - ), - results = listOf( - PythonResult( - "test-result", - "str", - "str" - ) - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument( - name = "test-parameter", - value = PythonReference(testFunction2Parameter) - ) - ) - ) - ) - ) - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |class test-class: - | def test-class-function(self, *, only-param='defaultValue'): - | self.instance.test-class-function(only-param) - | - |def function_module(param1, param2, param3): - | test-module.function_module(param1=param1, param2=param2, param3=param3) - | - |def test-function(*, test-parameter=42): - | test-module.test-function(test-parameter=test-parameter) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithNoClassesReturnsFormattedModuleContent() { // TODO - // given - val testFunction1Parameter1 = PythonParameter(name = "param1") - val testFunction1Parameter2 = PythonParameter(name = "param2") - val testFunction1Parameter3 = PythonParameter(name = "param3") - val testFunction2Parameter = PythonParameter( - "test-parameter", - "42", - PythonParameterAssignment.NAME_ONLY - ) - val testModule = PythonModule( - name = "test-module", - functions = mutableListOf( - PythonFunction( - name = "function_module", - parameters = mutableListOf( - testFunction1Parameter1, - testFunction1Parameter2, - testFunction1Parameter3 - ), - results = mutableListOf( - PythonResult( - "test-result", - "str", - "str", - "Lorem ipsum" - ) - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testFunction1Parameter1) - ), - PythonArgument( - name = "param2", - value = PythonReference(testFunction1Parameter2) - ), - PythonArgument( - name = "param3", - value = PythonReference(testFunction1Parameter3) - ) - ) - ) - ), - PythonFunction( - name = "test-function", - parameters = mutableListOf( - testFunction2Parameter - ), - results = mutableListOf( - PythonResult( - "test-result", - "str", - "str", - "Lorem ipsum" - ) - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument( - name = "test-parameter", - value = PythonReference(testFunction2Parameter) - ) - ) - ) - ) - ) - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |def function_module(param1, param2, param3): - | test-module.function_module(param1=param1, param2=param2, param3=param3) - | - |def test-function(*, test-parameter=42): - | test-module.test-function(test-parameter=test-parameter) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithNoFunctionsReturnsFormattedModuleContent() { // TODO - // given - val testClass = PythonClass( - name = "test-class", - constructor = PythonConstructor( - callToOriginalAPI = PythonCall(receiver = "test-module.test-class") - ), - originalClass = OriginalPythonClass("test-module.test-class") - ) - val testModule = PythonModule( - name = "test-module", - imports = mutableListOf( - PythonImport( - "test-import1", - "test-alias" - ) - ), - fromImports = mutableListOf( - PythonFromImport( - "test-from-import1", - "test-declaration1", - null - ) - ), - classes = mutableListOf(testClass) - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |class test-class: - | def __init__(): - | self.instance = test-module.test-class() - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithEmptyModuleReturnsEmptyString() { // TODO - // given - val testModule = PythonModule( - name = "test-module", - imports = mutableListOf( - PythonImport( - "test-import1", - "test-alias" - ) - ), - fromImports = mutableListOf( - PythonFromImport( - "test-from-import1", - "test-declaration1", - null - ) - ), - classes = mutableListOf(), - enums = mutableListOf(), - functions = mutableListOf() - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent = "" - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithBoundaryAnnotationReturnsFormattedModuleContent1() { // TODO - // given - val testParameter1 = PythonParameter( - name = "param1", - defaultValue = "5", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - testParameter1.boundary = Boundary( - true, - 2.0, - ComparisonOperator.LESS_THAN, - 10.0, - ComparisonOperator.LESS_THAN_OR_EQUALS - ) - val testParameter2 = PythonParameter( - "param2", - "5", - PythonParameterAssignment.NAME_ONLY - ) - testParameter2.boundary = Boundary( - false, - 5.0, - ComparisonOperator.LESS_THAN_OR_EQUALS, - 0.0, - ComparisonOperator.UNRESTRICTED - ) - val testParameter3 = PythonParameter( - "param3", - "5", - PythonParameterAssignment.NAME_ONLY - ) - testParameter3.boundary = Boundary( - false, - 0.0, - ComparisonOperator.UNRESTRICTED, - 10.0, - ComparisonOperator.LESS_THAN - ) - val testFunction = PythonFunction( - name = "function_module", - parameters = mutableListOf(testParameter1, testParameter2, testParameter3), - callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testParameter1) - ), - PythonArgument( - name = "param2", - value = PythonReference(testParameter2) - ), - PythonArgument( - name = "param3", - value = PythonReference(testParameter3) - ) - ) - ) - ) - val testModule = PythonModule( - name = "test-module", - functions = mutableListOf(testFunction), - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |def function_module(*, param1=5, param2=5, param3=5): - | if not (isinstance(param1, int) or (isinstance(param1, float) and param1.is_integer())): - | raise ValueError('param1 needs to be an integer, but {} was assigned.'.format(param1)) - | if not 2.0 < param1 <= 10.0: - | raise ValueError('Valid values of param1 must be in (2.0, 10.0], but {} was assigned.'.format(param1)) - | if not 5.0 <= param2: - | raise ValueError('Valid values of param2 must be greater than or equal to 5.0, but {} was assigned.'.format(param2)) - | if not param3 < 10.0: - | raise ValueError('Valid values of param3 must be less than 10.0, but {} was assigned.'.format(param3)) - | test-module.function_module(param1=param1, param2=param2, param3=param3) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithBoundaryAnnotationReturnsFormattedModuleContent2() { // TODO - // given - val testParameter = PythonParameter( - name = "param1", - defaultValue = "5", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - testParameter.boundary = Boundary( - false, - 2.0, - ComparisonOperator.LESS_THAN_OR_EQUALS, - 0.0, - ComparisonOperator.UNRESTRICTED - ) - val testFunction = PythonFunction( - name = "function_module", - parameters = listOf(testParameter), - callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testParameter) - ) - ) - ) - ) - val testModule = PythonModule( - name = "test-module", - functions = listOf(testFunction), - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |def function_module(*, param1=5): - | if not 2.0 <= param1: - | raise ValueError('Valid values of param1 must be greater than or equal to 2.0, but {} was assigned.'.format(param1)) - | test-module.function_module(param1=param1) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun `should create valid code for empty classes`() { // TODO - val testClass = PythonClass( - name = "TestClass", - constructor = PythonConstructor( - callToOriginalAPI = PythonCall( - receiver = "testModule.TestClass" - ) - ) - ) - - testClass.toPythonCode() shouldBe """ - |class TestClass: - | def __init__(): - | self.instance = testModule.TestClass() - """.trimMargin() - } - - @Test - fun buildClassReturnsFormattedClassWithOneFunction() { // TODO - // given - val testParameter = PythonParameter( - name = "only-param", - defaultValue = "'defaultValue'", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testClass = PythonClass( - name = "test-class", - constructor = PythonConstructor( - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-class", - arguments = listOf( - PythonArgument( - value = PythonReference(testParameter) - ) - ) - ) - ), - originalClass = OriginalPythonClass(qualifiedName = "test-module.test-class") - ) - - // when - val formattedClass = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | def __init__(self, *, only-param='defaultValue'): - | self.instance = test-module.test-class(only-param) - """.trimMargin() - formattedClass shouldBe expectedFormattedClass - } - - @Test - fun buildClassReturnsFormattedClassWithTwoFunctions() { // TODO - // given - val testMethod1Parameter = PythonParameter( - name = "only-param", - assignedBy = PythonParameterAssignment.POSITION_OR_NAME, - ) - val testMethod2Parameter = PythonParameter( - name = "only-param", - assignedBy = PythonParameterAssignment.POSITION_OR_NAME, - ) - val testClass = PythonClass( - name = "test-class", - methods = mutableListOf( - PythonFunction( - name = "test-class-function1", - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT, - ), - testMethod1Parameter - ), - callToOriginalAPI = PythonCall( - receiver = "self.instance.test-class-function1", - arguments = listOf( - PythonArgument(value = PythonReference(testMethod1Parameter)) - ) - ) - ), - PythonFunction( - name = "test-class-function2", - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testMethod2Parameter - ), - callToOriginalAPI = PythonCall( - receiver = "self.instance.test-class-function2", - arguments = listOf( - PythonArgument(value = PythonReference(testMethod2Parameter)) - ) - ) - ) - ) - ) - - // when - val formattedClass = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | def test-class-function1(self, only-param): - | self.instance.test-class-function1(only-param) - | - | def test-class-function2(self, only-param): - | self.instance.test-class-function2(only-param)""".trimMargin() - - formattedClass shouldBe expectedFormattedClass - } - - @Test - fun buildClassReturnsFormattedClassBasedOnOriginalDeclaration() { // TODO - // given - val testParameter1 = PythonParameter(name = "second-param") - val testParameter2 = PythonParameter(name = "third-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = "self.instance.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument( - name = "third-param", - value = PythonReference(testParameter2) - ) - ) - ) - ) - val testClass = PythonClass( - name = "test-class", - methods = mutableListOf(testFunction) - ) - - // when - val formattedClass = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | def test-function(self, second-param, third-param): - | self.instance.test-function(second-param, third-param=third-param)""".trimMargin() - formattedClass shouldBe expectedFormattedClass - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithNoParameters() { // TODO - // given - val testFunction = PythonFunction( - name = "test-function", - callToOriginalAPI = PythonCall(receiver = "test-module.test-function") - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(): - | test-module.test-function()""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionOnlyParameter() { // TODO - // given - val testParameter = PythonParameter( - name = "only-param", - defaultValue = "13", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter)) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(*, only-param=13): - | test-module.test-function(only-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionOrNameParameter() { // TODO - // given - val testParameter = PythonParameter( - name = "only-param", - defaultValue = "False", - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter)) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(*, only-param=False): - | test-module.test-function(only-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithNameOnlyParameter() { // TODO - // given - val testParameter = PythonParameter(name = "only-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument( - name = "only-param", - value = PythonReference(testParameter) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(only-param): - | test-module.test-function(only-param=only-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionAndPositionOrNameParameter() { // TODO - // given - - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument(value = PythonReference(testParameter2)) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param): - | test-module.test-function(first-param, second-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionAndPositionOrNameAndNameOnlyParameter() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testParameter3 = PythonParameter(name = "third-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = listOf( - testParameter1, - testParameter2, - testParameter3 - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument(value = PythonReference(testParameter2)), - PythonArgument( - name = "third-param", - value = PythonReference(testParameter3) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param, third-param): - | test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionAndNameOnlyParameter() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = listOf( - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument( - name = "second-param", - value = PythonReference(testParameter2) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param): - | test-module.test-function(first-param, second-param=second-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionOrNameAndNameOnlyParameter() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = listOf( - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument( - value = PythonReference(testParameter1) - ), - PythonArgument( - name = "second-param", - value = PythonReference(testParameter2) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param): - | test-module.test-function(first-param, second-param=second-param) - """.trimMargin() - - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionsReturnsFormattedFunctionBasedOnOriginalDeclaration() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testParameter3 = PythonParameter(name = "third-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter1, - testParameter2, - testParameter3 - ), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument(value = PythonReference(testParameter2)), - PythonArgument( - name = "third-param", - value = PythonReference(testParameter3) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param, third-param): - | test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildClassReturnsFormattedClassWithStaticMethodDecorator() { - // given - val testParameter = PythonParameter(name = "only-param") - val testClass = PythonClass( - name = "test-class", - methods = listOf( - PythonFunction( - name = "test-class-function1", - decorators = mutableListOf("staticmethod"), - parameters = listOf(testParameter), - callToOriginalAPI = PythonCall( - receiver = "test-module.test-class.test-class-function1", - arguments = listOf( - PythonArgument(value = PythonReference(testParameter)) - ) - ) - ) - ) - ) - - // when - val formattedClass: String = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | @staticmethod - | def test-class-function1(only-param): - | test-module.test-class.test-class-function1(only-param)""".trimMargin() - - formattedClass shouldBe expectedFormattedClass - } - - @Nested - inner class ModuleToPythonCode { - - @Test - fun `should import Enum if the module contains enums`() { - val testModule = PythonModule( - name = "testModule", - enums = listOf( - PythonEnum(name = "TestEnum") - ) - ) - - testModule.toPythonCode() shouldBe """ - |from enum import Enum - | - |class TestEnum(Enum): - | pass - | - """.trimMargin() - } - - @Test - fun `should not import Enum if the module does not contain enums`() { - val testModule = PythonModule(name = "testModule") - - testModule.toPythonCode() shouldBe "" - } - } - - @Nested - inner class FunctionToPythonCode { - - @Test - fun `should access value of enum parameters`() { - val testParameter = PythonParameter(name = "testParameter") - val testFunction = PythonFunction( - name = "testFunction", - parameters = listOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", - arguments = listOf( - PythonArgument( - value = PythonMemberAccess( - receiver = PythonReference(testParameter), - member = PythonReference(PythonAttribute(name = "value")) - ) - ) - ) - ) - ) - - testFunction.toPythonCode() shouldBe """ - |def testFunction(testParameter): - | testModule.testFunction(testParameter.value) - """.trimMargin() - } - - @Test - fun `should access attribute of parameter objects`() { - val testParameter = PythonParameter(name = "testGroup") - val testFunction = PythonFunction( - name = "testFunction", - parameters = listOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", - arguments = listOf( - PythonArgument( - value = PythonMemberAccess( - receiver = PythonReference(testParameter), - member = PythonReference( - PythonAttribute(name = "newParameter1") - ) - ) - ), - PythonArgument( - name = "oldParameter2", - value = PythonMemberAccess( - receiver = PythonReference(testParameter), - member = PythonReference( - PythonAttribute(name = "newParameter2") - ) - ) - ) - ) - ) - ) - - testFunction.toPythonCode() shouldBe """ - |def testFunction(testGroup): - | testModule.testFunction(testGroup.newParameter1, oldParameter2=testGroup.newParameter2) - """.trimMargin() - } - } - - @Nested - inner class EnumToPythonCode { - - @Test - fun `should create valid Python code for enums without instances`() { - val testEnum = PythonEnum(name = "TestEnum") - - testEnum.toPythonCode() shouldBe """ - |class TestEnum(Enum): - | pass - """.trimMargin() - } - - @Test - fun `should create valid Python code for enums with instances`() { - val testEnum = PythonEnum( - name = "TestEnum", - instances = listOf( - PythonEnumInstance( - name = "TestEnumInstance1", - value = "inst1" - ), - PythonEnumInstance( - name = "TestEnumInstance2", - value = "inst2" - ) - ) - ) - - testEnum.toPythonCode() shouldBe """ - |class TestEnum(Enum): - | TestEnumInstance1 = "inst1", - | TestEnumInstance2 = "inst2" - """.trimMargin() - } - } -} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt index bc350392c..e79277fe8 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt @@ -8,8 +8,12 @@ import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import de.unibonn.simpleml.SimpleMLStandaloneSetup import de.unibonn.simpleml.emf.annotationUsesOrEmpty import de.unibonn.simpleml.emf.argumentsOrEmpty @@ -88,14 +92,14 @@ class StubCodeGeneratorTest { ), PythonParameter( name = "testParameter", - typeInDocs = "int", - defaultValue = "10" + type = PythonStringifiedType("int"), + defaultValue = PythonStringifiedExpression("10") ) ), results = listOf( PythonResult( name = "testParameter", - type = "str" + type = PythonStringifiedType("str") ) ) ) @@ -453,7 +457,7 @@ class StubCodeGeneratorTest { fun `should store type`() { val pythonAttribute = PythonAttribute( name = "testAttribute", - typeInDocs = "str" + type = PythonStringifiedType("str") ) pythonAttribute @@ -741,7 +745,7 @@ class StubCodeGeneratorTest { fun `should store type`() { val pythonParameter = PythonParameter( name = "testParameter", - typeInDocs = "str" + type = PythonStringifiedType("str") ) pythonParameter @@ -759,7 +763,7 @@ class StubCodeGeneratorTest { fun `should store default value`() { val pythonParameter = PythonParameter( name = "testParameter", - defaultValue = "None" + defaultValue = PythonStringifiedExpression("None") ) pythonParameter @@ -860,7 +864,7 @@ class StubCodeGeneratorTest { fun `should store type`() { val pythonResult = PythonResult( name = "testResult", - type = "str" + type = PythonStringifiedType("str") ) val type = pythonResult.toSmlResult().type.shouldBeInstanceOf() @@ -1083,36 +1087,50 @@ class StubCodeGeneratorTest { inner class TypeConversions { @Test - fun `should convert bool to Boolean`() { - val smlType = "bool".toSmlType().shouldBeInstanceOf() + fun `should convert named types`() { + val smlType = PythonNamedType(PythonEnum(name = "MyEnum")).toSmlType().shouldBeInstanceOf() + smlType.declaration.name shouldBe "MyEnum" + smlType.isNullable.shouldBeFalse() + } + + @Test + fun `should convert stringified type 'bool' to Boolean`() { + val smlType = PythonStringifiedType("bool").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Boolean" smlType.isNullable.shouldBeFalse() } @Test - fun `should convert float to Float`() { - val smlType = "float".toSmlType().shouldBeInstanceOf() + fun `should convert stringified type 'float' to Float`() { + val smlType = PythonStringifiedType("float").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Float" smlType.isNullable.shouldBeFalse() } @Test - fun `should convert int to Int`() { - val smlType = "int".toSmlType().shouldBeInstanceOf() + fun `should convert stringified type 'int' to Int`() { + val smlType = PythonStringifiedType("int").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Int" smlType.isNullable.shouldBeFalse() } @Test - fun `should convert str to String`() { - val smlType = "str".toSmlType().shouldBeInstanceOf() + fun `should convert stringified type 'str' to String`() { + val smlType = PythonStringifiedType("str").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "String" smlType.isNullable.shouldBeFalse() } @Test fun `should convert other types to nullable Any`() { - val smlType = "other".toSmlType().shouldBeInstanceOf() + val smlType = PythonStringifiedType("other").toSmlType().shouldBeInstanceOf() + smlType.declaration.name shouldBe "Any" + smlType.isNullable.shouldBeTrue() + } + + @Test + fun `should convert null to nullable Any`() { + val smlType = null.toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Any" smlType.isNullable.shouldBeTrue() } @@ -1123,54 +1141,80 @@ class StubCodeGeneratorTest { @Test fun `should convert blank strings to null`() { - " ".toSmlExpression().shouldBeNull() + PythonStringifiedExpression(" ") + .toSmlExpression() + .shouldBeNull() } @Test fun `should convert False to a false boolean literal`() { - val smlBoolean = "False".toSmlExpression().shouldBeInstanceOf() + val smlBoolean = PythonStringifiedExpression("False") + .toSmlExpression() + .shouldBeInstanceOf() smlBoolean.isTrue.shouldBeFalse() } @Test fun `should convert True to a true boolean literal`() { - val smlBoolean = "True".toSmlExpression().shouldBeInstanceOf() + val smlBoolean = PythonStringifiedExpression("True") + .toSmlExpression() + .shouldBeInstanceOf() smlBoolean.isTrue.shouldBeTrue() } @Test fun `should convert None to a null literal`() { - "None".toSmlExpression().shouldBeInstanceOf() + PythonStringifiedExpression("None") + .toSmlExpression() + .shouldBeInstanceOf() } @Test fun `should convert ints to integer literals`() { - val smlInt = "123".toSmlExpression().shouldBeInstanceOf() + val smlInt = PythonStringifiedExpression("123") + .toSmlExpression() + .shouldBeInstanceOf() smlInt.value shouldBe 123 } @Test fun `should convert floats to float literals`() { - val smlFloat = "123.45".toSmlExpression().shouldBeInstanceOf() + val smlFloat = PythonStringifiedExpression("123.45") + .toSmlExpression() + .shouldBeInstanceOf() smlFloat.value shouldBe 123.45 } @Test fun `should convert single-quoted strings to string literals`() { - val smlString = "'string'".toSmlExpression().shouldBeInstanceOf() + val smlString = PythonStringifiedExpression("'string'") + .toSmlExpression() + .shouldBeInstanceOf() smlString.value shouldBe "string" } @Test fun `should convert double-quoted strings to string literals`() { - val smlString = "\"string\"".toSmlExpression().shouldBeInstanceOf() + val smlString = PythonStringifiedExpression("\"string\"") + .toSmlExpression() + .shouldBeInstanceOf() smlString.value shouldBe "string" } @Test - fun `should convert other values to '###invalid###' strings`() { - val smlString = "unknown".toSmlExpression().shouldBeInstanceOf() + fun `should convert other stringified expressions to '###invalid###' strings`() { + val smlString = PythonStringifiedExpression("unknown") + .toSmlExpression() + .shouldBeInstanceOf() smlString.value shouldBe "###invalid###unknown###" } + + @Test + fun `should convert other expressions to '###invalid###' strings`() { + val smlString = PythonString("unknown") + .toSmlExpression() + .shouldBeInstanceOf() + smlString.value shouldBe "###invalid###PythonString(value=unknown)###" + } } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt new file mode 100644 index 000000000..d6c32e7a3 --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt @@ -0,0 +1,720 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.model.BoundaryAnnotation +import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.DefaultBoolean +import com.larsreimann.api_editor.model.DefaultNumber +import com.larsreimann.api_editor.model.EnumAnnotation +import com.larsreimann.api_editor.model.EnumPair +import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.MoveAnnotation +import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.transformation.transform +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ConstructorPythonCodeGeneratorTest { + private lateinit var testParameter1: PythonParameter + private lateinit var testParameter2: PythonParameter + private lateinit var testParameter3: PythonParameter + private lateinit var testParameterSelf: PythonParameter + private lateinit var testClass: PythonClass + private lateinit var testFunction: PythonFunction + private lateinit var testModule: PythonModule + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testParameterSelf = PythonParameter( + name = "self" + ) + testParameter1 = PythonParameter( + name = "testParameter1" + ) + testParameter2 = PythonParameter( + name = "testParameter2" + ) + testParameter3 = PythonParameter( + name = "testParameter3" + ) + testFunction = PythonFunction( + name = "__init__", + parameters = mutableListOf( + testParameterSelf, + testParameter1, + testParameter2, + testParameter3 + ) + ) + testClass = PythonClass(name = "testClass", methods = mutableListOf(testFunction)) + testModule = PythonModule( + name = "testModule", + classes = mutableListOf(testClass) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = mutableListOf(testModule) + ) + } + + @Test + fun `should process Boundary- and GroupAnnotation on constructor level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, testParameter2): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and OptionalAnnotation on constructor level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and RequiredAnnotation on constructor level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and OptionalAnnotation on constructor level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter2, testParameter3, *, testParameter1=0.5): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | self.testParameter1 = testParameter1 + | + | self.instance = testModule.testClass(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RequiredAnnotation on constructor level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.UNRESTRICTED + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testParameter2, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 < testParameter1: + | raise ValueError(f'Valid values of testParameter1 must be greater than 0.0, but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on constructor level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Required- and GroupAnnotation on constructor level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and RequiredAnnotation on constructor level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(self, testParameter1: TestEnum, testParameter2, testParameter3): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testParameter1.value, testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and RequiredAnnotation on constructor level`() { + // given + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and OptionalAnnotation on constructor level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group-, Required- and OptionalAnnotation on constructor level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testParameter3.annotations.add( + RequiredAnnotation + ) + testParameter3.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group-, OptionalAnnotation on constructor with RenameAnnotation on class level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testClass.annotations.add( + RenameAnnotation("renamedTestClass") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class renamedTestClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, GroupAnnotation on constructor with Rename-, MoveAnnotation on class level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testClass.annotations.add( + RenameAnnotation("renamedTestClass") + ) + testClass.annotations.add( + MoveAnnotation("movedTestModule") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class renamedTestClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + testPackage.modules[0].name shouldBe "movedTestModule" + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt new file mode 100644 index 000000000..117dbeca1 --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -0,0 +1,697 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.model.BoundaryAnnotation +import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.DefaultBoolean +import com.larsreimann.api_editor.model.DefaultNumber +import com.larsreimann.api_editor.model.EnumAnnotation +import com.larsreimann.api_editor.model.EnumPair +import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.MoveAnnotation +import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.transformation.transform +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class FunctionPythonCodeGeneratorTest { + private lateinit var testParameter1: PythonParameter + private lateinit var testParameter2: PythonParameter + private lateinit var testParameter3: PythonParameter + private lateinit var testFunction: PythonFunction + private lateinit var testModule: PythonModule + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testParameter1 = PythonParameter( + name = "testParameter1", + ) + testParameter2 = PythonParameter( + name = "testParameter2", + ) + testParameter3 = PythonParameter( + name = "testParameter3", + ) + testFunction = PythonFunction( + name = "testFunction", + parameters = mutableListOf( + testParameter1, + testParameter2, + testParameter3 + ) + ) + testModule = PythonModule( + name = "testModule", + functions = mutableListOf(testFunction) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = mutableListOf(testModule) + ) + } + + @Test + fun `should process Boundary- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter1, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter3 = testParameter3 + | + |def testFunction(testGroup: TestGroup, testParameter2): + | return testModule.testFunction(testGroup.testParameter1, testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and RequiredAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + RequiredAnnotation + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and OptionalAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |def testFunction(testParameter2, testParameter3, *, testParameter1=0.5): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | return testModule.testFunction(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RequiredAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |def testFunction(testParameter1, testParameter2, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | return testModule.testFunction(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |def testFunction(testGroup: TestGroup, testParameter3): + | return testModule.testFunction(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Required- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |def testFunction(testGroup: TestGroup, testParameter3): + | return testModule.testFunction(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and RequiredAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |def testFunction(testParameter1: TestEnum, testParameter2, testParameter3): + | return testModule.testFunction(testParameter1.value, testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and RequiredAnnotation on function level`() { + // given + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testParameter1.defaultValue = PythonStringifiedExpression("defaultValue") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + |def testFunction(testGroup: TestGroup, *, testParameter1=defaultValue): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group-, Required- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testParameter3.annotations.add( + RequiredAnnotation + ) + testParameter3.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RenameAnnotation on parameter on a function with RenameAnnotation`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + RenameAnnotation("renamedTestParameter1") + ) + testFunction.annotations.add( + RenameAnnotation("renamedTestFunction") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |def renamedTestFunction(renamedTestParameter1, testParameter2, testParameter3): + | if not (isinstance(renamedTestParameter1, int) or (isinstance(renamedTestParameter1, float) and renamedTestParameter1.is_integer())): + | raise ValueError(f'renamedTestParameter1 needs to be an integer, but {renamedTestParameter1} was assigned.') + | if not 0.0 <= renamedTestParameter1 <= 1.0: + | raise ValueError(f'Valid values of renamedTestParameter1 must be in [0.0, 1.0], but {renamedTestParameter1} was assigned.') + | + | return testModule.testFunction(renamedTestParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on function with MoveAnnotation`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testFunction.annotations.add( + MoveAnnotation("movedTestModule") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |def testFunction(testGroup: TestGroup, testParameter3): + | return testModule.testFunction(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + testPackage.modules[0].name shouldBe "movedTestModule" + } + + @Test + fun `should process Rename-, Boundary-, Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testParameter2.annotations.add( + RenameAnnotation("renamedTestParameter2") + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, renamedTestParameter2=0.5): + | if not (isinstance(renamedTestParameter2, int) or (isinstance(renamedTestParameter2, float) and renamedTestParameter2.is_integer())): + | raise ValueError(f'renamedTestParameter2 needs to be an integer, but {renamedTestParameter2} was assigned.') + | if not 0.0 <= renamedTestParameter2 <= 1.0: + | raise ValueError(f'Valid values of renamedTestParameter2 must be in [0.0, 1.0], but {renamedTestParameter2} was assigned.') + | + | self.testParameter3 = testParameter3 + | self.renamedTestParameter2 = renamedTestParameter2 + | + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.renamedTestParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt new file mode 100644 index 000000000..30d390bda --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt @@ -0,0 +1,640 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.model.BoundaryAnnotation +import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.DefaultBoolean +import com.larsreimann.api_editor.model.DefaultNumber +import com.larsreimann.api_editor.model.EnumAnnotation +import com.larsreimann.api_editor.model.EnumPair +import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonInt +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.transformation.transform +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MethodPythonCodeGeneratorTest { + private lateinit var testParameterSelf: PythonParameter + private lateinit var testParameter1: PythonParameter + private lateinit var testParameter2: PythonParameter + private lateinit var testParameter3: PythonParameter + private lateinit var testClass: PythonClass + private lateinit var testFunction: PythonFunction + private lateinit var testModule: PythonModule + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testParameterSelf = PythonParameter( + name = "self" + ) + testParameter1 = PythonParameter( + name = "testParameter1" + ) + testParameter2 = PythonParameter( + name = "testParameter2" + ) + testParameter3 = PythonParameter( + name = "testParameter3" + ) + testFunction = PythonFunction( + name = "testMethod", + parameters = mutableListOf( + testParameterSelf, + testParameter1, + testParameter2, + testParameter3 + ) + ) + testClass = PythonClass(name = "testClass", methods = mutableListOf(testFunction)) + testModule = PythonModule( + name = "testModule", + classes = mutableListOf(testClass) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = mutableListOf(testModule) + ) + } + + @Test + fun `should process Boundary- and GroupAnnotation on class method level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, testParameter2): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and OptionalAnnotation on class method level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not testParameter2 < 1.0: + | raise ValueError(f'Valid values of testParameter2 must be less than 1.0, but {testParameter2} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and RequiredAnnotation on class method level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonInt(0) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and OptionalAnnotation on class method level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter2, testParameter3, *, testParameter1=0.5): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | return self.instance.testMethod(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RequiredAnnotation on class method level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.UNRESTRICTED + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testParameter2, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 < testParameter1: + | raise ValueError(f'Valid values of testParameter1 must be greater than 0.0, but {testParameter1} was assigned.') + | + | return self.instance.testMethod(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on class method level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Required- and GroupAnnotation on class method level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and RequiredAnnotation on class method level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1: TestEnum, testParameter2, testParameter3): + | return self.instance.testMethod(testParameter1.value, testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and RequiredAnnotation on class method level`() { + // given + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and OptionalAnnotation on class method level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group-, Required- and OptionalAnnotation on class method level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testParameter3.annotations.add( + RequiredAnnotation + ) + testParameter3.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process GroupAnnotation on class method with RenameAnnotation on parameter level`() { + // given + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testParameter1.annotations.add( + RenameAnnotation("renamedTestParameter1") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.renamedTestParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, renamedTestParameter1, testParameter2): + | self.renamedTestParameter1 = renamedTestParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/PythonCodeGeneratorTest.kt new file mode 100644 index 000000000..52d2d9211 --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/PythonCodeGeneratorTest.kt @@ -0,0 +1,1302 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.codegen.toPythonCodeOrNull +import com.larsreimann.api_editor.model.Boundary +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS +import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED +import com.larsreimann.api_editor.model.PythonParameterAssignment +import com.larsreimann.api_editor.mutable_model.PythonArgument +import com.larsreimann.api_editor.mutable_model.PythonAttribute +import com.larsreimann.api_editor.mutable_model.PythonBoolean +import com.larsreimann.api_editor.mutable_model.PythonCall +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonConstructor +import com.larsreimann.api_editor.mutable_model.PythonEnum +import com.larsreimann.api_editor.mutable_model.PythonEnumInstance +import com.larsreimann.api_editor.mutable_model.PythonFloat +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonInt +import com.larsreimann.api_editor.mutable_model.PythonMemberAccess +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class PythonCodeGeneratorTest { + + /* **************************************************************************************************************** + * Declarations + * ****************************************************************************************************************/ + + @Nested + inner class AttributeToPythonCode { + + @Test + fun `should handle attributes without type and default value`() { + val testAttribute = PythonAttribute( + name = "attr" + ) + + testAttribute.toPythonCode() shouldBe "self.attr" + } + + @Test + fun `should handle attributes with type but without default value`() { + val testAttribute = PythonAttribute( + name = "attr", + type = PythonStringifiedType("int") + ) + + testAttribute.toPythonCode() shouldBe "self.attr: int" + } + + @Test + fun `should handle attributes without type but with default value`() { + val testAttribute = PythonAttribute( + name = "attr", + value = PythonStringifiedExpression("1") + ) + + testAttribute.toPythonCode() shouldBe "self.attr = 1" + } + + @Test + fun `should handle attributes with type and default value`() { + val testAttribute = PythonAttribute( + name = "attr", + type = PythonStringifiedType("int"), + value = PythonStringifiedExpression("1") + ) + + testAttribute.toPythonCode() shouldBe "self.attr: int = 1" + } + } + + @Nested + inner class ClassToPythonCode { + + @Test + fun `should create valid code for classes without constructor and methods`() { + val testClass = PythonClass( + name = "TestClass" + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | pass + """.trimMargin() + } + + @Test + fun `should create valid code for classes without constructor but with methods`() { + val testClass = PythonClass( + name = "TestClass", + methods = listOf( + PythonFunction(name = "testFunction1"), + PythonFunction(name = "testFunction2") + ) + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def testFunction1(): + | pass + | + | def testFunction2(): + | pass + """.trimMargin() + } + + @Test + fun `should create valid code for classes with constructor but without methods`() { + val testClass = PythonClass( + name = "TestClass", + constructor = PythonConstructor() + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def __init__(): + | pass + """.trimMargin() + } + + @Test + fun `should create valid code for classes with constructor and methods`() { + val testClass = PythonClass( + name = "TestClass", + constructor = PythonConstructor(), + methods = listOf( + PythonFunction(name = "testFunction1"), + PythonFunction(name = "testFunction2") + ) + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def __init__(): + | pass + | + | def testFunction1(): + | pass + | + | def testFunction2(): + | pass + """.trimMargin() + } + + @Test + fun `should not indent blank lines`() { + val testClass = PythonClass( + name = "TestClass", + constructor = PythonConstructor( + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("testModule.TestClass") + ) + ), + attributes = listOf( + PythonAttribute( + name = "testAttribute", + value = PythonInt(1) + ) + ) + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def __init__(): + | self.testAttribute = 1 + | + | self.instance = testModule.TestClass() + """.trimMargin() + } + } + + @Nested + inner class ConstructorToPythonCode { + + private lateinit var callToOriginalAPI: PythonCall + private lateinit var testClass: PythonClass + private lateinit var parametersWithBoundaries: List + + @BeforeEach + fun reset() { + callToOriginalAPI = PythonCall( + PythonReference( + PythonClass(name = "OriginalClass") + ) + ) + testClass = PythonClass( + name = "TestClass", + attributes = listOf( + PythonAttribute( + name = "testAttribute1", + value = PythonInt(1) + ), + PythonAttribute( + name = "testAttribute2", + value = PythonInt(2) + ) + ) + ) + parametersWithBoundaries = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "testParameter1", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + ), + PythonParameter( + name = "testParameter2", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + ) + ) + } + + @Test + fun `should create code for parameters`() { + val testConstructor = PythonConstructor( + parameters = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ), + PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ), + PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + ) + ) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, positionOnly, /, positionOrName, *, nameOnly): + | pass + """.trimMargin() + } + + @Test + fun `should handle constructors (no boundaries, no attributes, no call)`() { + val testConstructor = PythonConstructor() + + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | pass + """.trimMargin() + } + + @Test + fun `should handle constructors (no boundaries, no attributes, call)`() { + val testConstructor = PythonConstructor(callToOriginalAPI = callToOriginalAPI) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | self.instance = OriginalClass() + """.trimMargin() + } + + @Test + fun `should handle constructors (no boundaries, attributes, no call)`() { + val testConstructor = PythonConstructor() + testClass.constructor = testConstructor + + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + """.trimMargin() + } + + @Test + fun `should handle constructors (no boundaries, attributes, call)`() { + val testConstructor = PythonConstructor(callToOriginalAPI = callToOriginalAPI) + testClass.constructor = testConstructor + + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + | + | self.instance = OriginalClass() + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, no attributes, no call)`() { + val testConstructor = PythonConstructor(parameters = parametersWithBoundaries) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | if not 0.0 < testParameter2: + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, no attributes, call)`() { + val testConstructor = PythonConstructor( + callToOriginalAPI = callToOriginalAPI, + parameters = parametersWithBoundaries + ) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | if not 0.0 < testParameter2: + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') + | + | self.instance = OriginalClass() + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, attributes, no call)`() { + val testConstructor = PythonConstructor( + parameters = parametersWithBoundaries + ) + testClass.constructor = testConstructor + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | if not 0.0 < testParameter2: + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') + | + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, attributes, call)`() { + val testConstructor = PythonConstructor( + callToOriginalAPI = callToOriginalAPI, + parameters = parametersWithBoundaries + ) + testClass.constructor = testConstructor + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | if not 0.0 < testParameter2: + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') + | + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + | + | self.instance = OriginalClass() + """.trimMargin() + } + } + + @Nested + inner class EnumToPythonCode { + + @Test + fun `should create valid Python code for enums without instances`() { + val testEnum = PythonEnum(name = "TestEnum") + + testEnum.toPythonCode() shouldBe """ + |class TestEnum(Enum): + | pass + """.trimMargin() + } + + @Test + fun `should create valid Python code for enums with instances`() { + val testEnum = PythonEnum( + name = "TestEnum", + instances = listOf( + PythonEnumInstance( + name = "TestEnumInstance1", + value = PythonString("inst1") + ), + PythonEnumInstance( + name = "TestEnumInstance2", + value = PythonString("inst2") + ) + ) + ) + + testEnum.toPythonCode() shouldBe """ + |class TestEnum(Enum): + | TestEnumInstance1 = 'inst1', + | TestEnumInstance2 = 'inst2' + """.trimMargin() + } + } + + @Nested + inner class EnumInstanceToPythonCode { + + @Test + fun `should create Python code`() { + val testEnumInstance = PythonEnumInstance( + name = "TestEnumInstance1", + value = PythonString("inst1") + ) + + testEnumInstance.toPythonCode() shouldBe "TestEnumInstance1 = 'inst1'" + } + } + + @Nested + inner class FunctionToPythonCode { + + private lateinit var callToOriginalAPI: PythonCall + private lateinit var parametersWithBoundaries: List + + @BeforeEach + fun reset() { + callToOriginalAPI = PythonCall( + PythonReference( + PythonFunction(name = "testModule.testFunction") + ) + ) + parametersWithBoundaries = listOf( + PythonParameter( + name = "testParameter1", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + ), + PythonParameter( + name = "testParameter2", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + ) + ) + } + + @Test + fun `should add staticmethod decorator to static methods`() { + val testFunction = PythonFunction( + name = "testFunction", + decorators = mutableListOf("staticmethod") + ) + PythonClass( + name = "TestClass", + methods = listOf(testFunction) + ) + + testFunction.toPythonCode() shouldBe """ + |@staticmethod + |def testFunction(): + | pass + """.trimMargin() + } + + @Test + fun `should create code for parameters`() { + val testFunction = PythonFunction( + name = "testFunction", + parameters = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ), + PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ), + PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + ) + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(self, positionOnly, /, positionOrName, *, nameOnly): + | pass + """.trimMargin() + } + + @Test + fun `should handle functions (no boundaries, no call)`() { + val testFunction = PythonFunction( + name = "testFunction" + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(): + | pass + """.trimMargin() + } + + @Test + fun `should handle functions (no boundaries, call)`() { + val testFunction = PythonFunction( + name = "testFunction", + callToOriginalAPI = callToOriginalAPI + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(): + | return testModule.testFunction() + """.trimMargin() + } + + @Test + fun `should handle functions (boundaries, no call)`() { + val testFunction = PythonFunction( + name = "testFunction", + parameters = parametersWithBoundaries + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | if not 0.0 < testParameter2: + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle functions (boundaries, call)`() { + val testFunction = PythonFunction( + name = "testFunction", + callToOriginalAPI = callToOriginalAPI, + parameters = parametersWithBoundaries + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | if not 0.0 < testParameter2: + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') + | + | return testModule.testFunction() + """.trimMargin() + } + } + + @Nested + inner class ModuleToPythonCode { + + private lateinit var testModule: PythonModule + private lateinit var testClasses: List + private lateinit var testFunctions: List + private lateinit var testEnum: PythonEnum + + @BeforeEach + fun reset() { + testModule = PythonModule(name = "testModule") + testClasses = listOf( + PythonClass( + name = "TestClass", + methods = listOf( + PythonFunction(name = "testMethod1"), + PythonFunction( + name = "testMethodWithOriginalMethod", + decorators = mutableListOf("staticmethod"), + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("originalModule.testMethod") + ) + ) + ) + ), + PythonClass( + name = "TestClassWithConstructor", + constructor = PythonConstructor( + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("originalModule.TestClass") + ) + ) + ) + ) + testFunctions = listOf( + PythonFunction(name = "testFunction"), + PythonFunction( + name = "testFunctionWithOriginalFunction", + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("originalModule2.testFunction") + ) + ) + ) + testEnum = PythonEnum(name = "TestEnum") + } + + @Test + fun `should create Python code for modules (no classes, no functions, no enums)`() { + testModule.toPythonCode() shouldBe "from __future__ import annotations\n" + } + + @Test + fun `should create Python code for modules (no classes, no functions, enums)`() { + testModule.enums += testEnum + + testModule.toPythonCode() shouldBe """ + |from __future__ import annotations + |from enum import Enum + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (no classes, functions, no enums)`() { + testModule.functions += testFunctions + + testModule.toPythonCode() shouldBe """ + |import originalModule2 + | + |from __future__ import annotations + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule2.testFunction() + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (no classes, functions, enums)`() { + testModule.functions += testFunctions + testModule.enums += testEnum + + testModule.toPythonCode() shouldBe """ + |import originalModule2 + | + |from __future__ import annotations + |from enum import Enum + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule2.testFunction() + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, no functions, no enums)`() { + testModule.classes += testClasses + + testModule.toPythonCode() shouldBe """ + |import originalModule + | + |from __future__ import annotations + | + |class TestClass: + | def testMethod1(): + | pass + | + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() + | + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, no functions, enums)`() { + testModule.classes += testClasses + testModule.enums += testEnum + + testModule.toPythonCode() shouldBe """ + |import originalModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestClass: + | def testMethod1(): + | pass + | + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() + | + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, functions, no enums)`() { + testModule.classes += testClasses + testModule.functions += testFunctions + + testModule.toPythonCode() shouldBe """ + |import originalModule + |import originalModule2 + | + |from __future__ import annotations + | + |class TestClass: + | def testMethod1(): + | pass + | + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() + | + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule2.testFunction() + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, functions, enums)`() { + testModule.classes += testClasses + testModule.functions += testFunctions + testModule.enums += testEnum + + testModule.toPythonCode() shouldBe """ + |import originalModule + |import originalModule2 + | + |from __future__ import annotations + |from enum import Enum + | + |class TestClass: + | def testMethod1(): + | pass + | + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() + | + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule2.testFunction() + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + } + + @Nested + inner class ParameterListToPythonCode { + + private lateinit var implicit: PythonParameter + private lateinit var positionOnly: PythonParameter + private lateinit var positionOrName: PythonParameter + private lateinit var nameOnly: PythonParameter + + @BeforeEach + fun reset() { + implicit = PythonParameter( + name = "implicit", + assignedBy = PythonParameterAssignment.IMPLICIT + ) + positionOnly = PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ) + positionOrName = PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ) + nameOnly = PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf() + + parameters.toPythonCode() shouldBe "" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(nameOnly) + + parameters.toPythonCode() shouldBe "*, nameOnly" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(positionOrName) + + parameters.toPythonCode() shouldBe "positionOrName" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "positionOrName, *, nameOnly" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(positionOnly) + + parameters.toPythonCode() shouldBe "positionOnly, /" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(positionOnly, nameOnly) + + parameters.toPythonCode() shouldBe "positionOnly, /, *, nameOnly" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(positionOnly, positionOrName) + + parameters.toPythonCode() shouldBe "positionOnly, /, positionOrName" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(positionOnly, positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "positionOnly, /, positionOrName, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(implicit) + + parameters.toPythonCode() shouldBe "implicit" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(implicit, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(implicit, positionOrName) + + parameters.toPythonCode() shouldBe "implicit, positionOrName" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(implicit, positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOrName, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(implicit, positionOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(implicit, positionOnly, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { + val parameters = listOf(implicit, positionOnly, positionOrName) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /, positionOrName" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { + val parameters = listOf(implicit, positionOnly, positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /, positionOrName, *, nameOnly" + } + } + + @Nested + inner class ParameterToPythonCode { + + @Test + fun `should handle parameters without type and default value`() { + val testParameter = PythonParameter( + name = "param" + ) + + testParameter.toPythonCode() shouldBe "param" + } + + @Test + fun `should handle parameters with type but without default value`() { + val testParameter = PythonParameter( + name = "param", + type = PythonStringifiedType("int") + ) + + testParameter.toPythonCode() shouldBe "param: int" + } + + @Test + fun `should handle parameters without type but with default value`() { + val testParameter = PythonParameter( + name = "param", + defaultValue = PythonStringifiedExpression("1") + ) + + testParameter.toPythonCode() shouldBe "param=1" + } + + @Test + fun `should handle parameters with type and default value`() { + val testParameter = PythonParameter( + name = "param", + type = PythonStringifiedType("int"), + defaultValue = PythonStringifiedExpression("1") + ) + + testParameter.toPythonCode() shouldBe "param: int = 1" + } + } + + /* **************************************************************************************************************** + * Expressions + * ****************************************************************************************************************/ + + @Nested + inner class ExpressionToPythonCode { + + @Test + fun `should handle false boolean`() { + val expression = PythonBoolean(false) + expression.toPythonCode() shouldBe "False" + } + + @Test + fun `should handle true boolean`() { + val expression = PythonBoolean(true) + expression.toPythonCode() shouldBe "True" + } + + @Test + fun `should handle calls`() { + val expression = PythonCall( + receiver = PythonStringifiedExpression("function"), + arguments = listOf( + PythonArgument(value = PythonInt(1)), + PythonArgument( + name = "param", + value = PythonInt(1) + ) + ) + ) + expression.toPythonCode() shouldBe "function(1, param=1)" + } + + @Test + fun `should handle floats`() { + val expression = PythonFloat(1.0) + expression.toPythonCode() shouldBe "1.0" + } + + @Test + fun `should handle ints`() { + val expression = PythonInt(1) + expression.toPythonCode() shouldBe "1" + } + + @Test + fun `should handle member accesses`() { + val expression = PythonMemberAccess( + receiver = PythonReference(PythonParameter(name = "param")), + member = PythonReference(PythonAttribute(name = "value")) + ) + expression.toPythonCode() shouldBe "param.value" + } + + @Test + fun `should handle references`() { + val expression = PythonReference(PythonParameter("param")) + expression.toPythonCode() shouldBe "param" + } + + @Test + fun `should handle strings`() { + val expression = PythonString("string") + expression.toPythonCode() shouldBe "'string'" + } + + @Test + fun `should handle stringified expression`() { + val expression = PythonStringifiedExpression("1") + expression.toPythonCode() shouldBe "1" + } + } + + /* **************************************************************************************************************** + * Types + * ****************************************************************************************************************/ + + @Nested + inner class TypeToPythonCodeOrNull { + + @Test + fun `should handle named types`() { + val type = PythonNamedType(PythonEnum("TestEnum")) + type.toPythonCodeOrNull() shouldBe "TestEnum" + } + + @Test + fun `should convert stringified type 'bool' to Boolean`() { + val smlType = PythonStringifiedType("bool") + smlType.toPythonCodeOrNull() shouldBe "bool" + } + + @Test + fun `should convert stringified type 'float' to Float`() { + val smlType = PythonStringifiedType("float") + smlType.toPythonCodeOrNull() shouldBe "float" + } + + @Test + fun `should convert stringified type 'int' to Int`() { + val smlType = PythonStringifiedType("int") + smlType.toPythonCodeOrNull() shouldBe "int" + } + + @Test + fun `should convert stringified type 'str' to String`() { + val smlType = PythonStringifiedType("str") + smlType.toPythonCodeOrNull() shouldBe "str" + } + + @Test + fun `should return null for other types`() { + val type = PythonStringifiedType("") + type.toPythonCodeOrNull().shouldBeNull() + } + } + + /* **************************************************************************************************************** + * Other + * ****************************************************************************************************************/ + + @Nested + inner class ArgumentToPythonCode { + + @Test + fun `should handle positional arguments`() { + val testArgument = PythonArgument(value = PythonInt(1)) + + testArgument.toPythonCode() shouldBe "1" + } + + @Test + fun `should handle named arguments`() { + val testArgument = PythonArgument( + name = "arg", + value = PythonInt(1) + ) + + testArgument.toPythonCode() shouldBe "arg=1" + } + } + + @Nested + inner class BoundaryToPythonCode { + + @Test + fun `should add an extra check for discrete boundaries`() { + val boundary = Boundary( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not (isinstance(testParameter, int) or (isinstance(testParameter, float) and testParameter.is_integer())): + | raise ValueError(f'testParameter needs to be an integer, but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (UNRESTRICTED, UNRESTRICTED)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe "" + } + + @Test + fun `should handle continuous boundaries (UNRESTRICTED, LESS_THAN)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not testParameter < 1.0: + | raise ValueError(f'Valid values of testParameter must be less than 1.0, but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (UNRESTRICTED, LESS_THAN_OR_EQUALS)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not testParameter <= 1.0: + | raise ValueError(f'Valid values of testParameter must be less than or equal to 1.0, but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN, UNRESTRICTED)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 < testParameter: + | raise ValueError(f'Valid values of testParameter must be greater than 0.0, but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN, LESS_THAN)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 < testParameter < 1.0: + | raise ValueError(f'Valid values of testParameter must be in (0.0, 1.0), but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN, LESS_THAN_OR_EQUALS)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 < testParameter <= 1.0: + | raise ValueError(f'Valid values of testParameter must be in (0.0, 1.0], but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN_OR_EQUALS, UNRESTRICTED)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 <= testParameter: + | raise ValueError(f'Valid values of testParameter must be greater than or equal to 0.0, but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN_OR_EQUALS, LESS_THAN)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 <= testParameter < 1.0: + | raise ValueError(f'Valid values of testParameter must be in [0.0, 1.0), but {testParameter} was assigned.') + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 <= testParameter <= 1.0: + | raise ValueError(f'Valid values of testParameter must be in [0.0, 1.0], but {testParameter} was assigned.') + """.trimMargin() + } + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt index d211cd3de..a43d306d9 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt @@ -131,7 +131,7 @@ class ApplicationTest { ), GroupAnnotation( groupName = "test-group", - parameters = listOf( + parameters = mutableListOf( "test-parameter" ) ), diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt index 78052efbe..1333a3058 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt @@ -10,14 +10,16 @@ import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.transformation.processing_exceptions.ConflictingEnumException import io.kotest.assertions.asClue import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull @@ -51,7 +53,7 @@ class EnumAnnotationProcessorTest { name = "testFunction", parameters = listOf(testParameter), callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", + receiver = PythonStringifiedExpression("testModule.testFunction"), arguments = listOf( PythonArgument( value = PythonReference(testParameter) @@ -97,16 +99,32 @@ class EnumAnnotationProcessorTest { fun `should process EnumAnnotations on parameter level`() { testPackage.processEnumAnnotations() - testParameter.typeInDocs shouldBe "TestEnum" + val type = testParameter.type + type.shouldBeInstanceOf() + + val declaration = type.declaration + declaration.shouldBeInstanceOf() + declaration.name shouldBe "TestEnum" } @Test fun `should process EnumAnnotations on module level`() { testPackage.processEnumAnnotations() - testModule.enums[0].name shouldBe "TestEnum" - testModule.enums[0].instances shouldContain PythonEnumInstance("name1", "value1") - testModule.enums[0].instances shouldContain PythonEnumInstance("name2", "value2") + val enum = testModule.enums[0] + enum.name shouldBe "TestEnum" + + val instances = enum.instances + instances.shouldHaveSize(2) + + instances[0].asClue { + it.name shouldBe "name1" + it.value shouldBe PythonString("value1") + } + instances[1].asClue { + it.name shouldBe "name2" + it.value shouldBe PythonString("value2") + } } @Test @@ -114,14 +132,25 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name2", "value2") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name2", + value = PythonString("value2") + ) ) ) testModule.enums += mutableEnum testPackage.processEnumAnnotations() - testParameter.typeInDocs shouldBe "TestEnum" + val type = testParameter.type + type.shouldBeInstanceOf() + + val declaration = type.declaration + declaration.shouldBeInstanceOf() + declaration.name shouldBe "TestEnum" } @Test @@ -129,16 +158,35 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name2", "value2") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name2", + value = PythonString("value2") + ) ) ) testModule.enums.add(mutableEnum) testPackage.processEnumAnnotations() - testModule.enums[0].name shouldBe "TestEnum" - testModule.enums[0].instances shouldContain PythonEnumInstance("name1", "value1") - testModule.enums[0].instances shouldContain PythonEnumInstance("name2", "value2") + testModule.enums.shouldHaveSize(1) + + val enum = testModule.enums[0] + enum.name shouldBe "TestEnum" + + val instances = enum.instances + instances.shouldHaveSize(2) + + instances[0].asClue { + it.name shouldBe "name1" + it.value shouldBe PythonString("value1") + } + instances[1].asClue { + it.name shouldBe "name2" + it.value shouldBe PythonString("value2") + } } @Test @@ -146,8 +194,14 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name2", "value2") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name2", + value = PythonString("value2") + ) ) ) testModule.enums.add(mutableEnum) @@ -180,8 +234,14 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name3", "value3") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name3", + value = PythonString("value3") + ) ) ) testModule.enums += mutableEnum diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt index bdf3357e4..1d49c1b60 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt @@ -9,9 +9,11 @@ import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.transformation.processing_exceptions.ConflictingGroupException import io.kotest.assertions.asClue import io.kotest.assertions.throwables.shouldThrowExactly @@ -52,7 +54,7 @@ class GroupAnnotationProcessorTest { ) ), callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", + receiver = PythonStringifiedExpression("testModule.testFunction"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument(value = PythonReference(testParameter2)), @@ -90,7 +92,7 @@ class GroupAnnotationProcessorTest { val secondArgumentValue = secondArgument.value.shouldBeInstanceOf() secondArgumentValue.receiver.asClue { it.shouldBeInstanceOf() - it.declaration?.name shouldBe "TestGroup" + it.declaration?.name shouldBe "testGroup" } secondArgumentValue.member.asClue { it.shouldNotBeNull() @@ -102,7 +104,7 @@ class GroupAnnotationProcessorTest { val thirdArgumentValue = thirdArgument.value.shouldBeInstanceOf() thirdArgumentValue.receiver.asClue { it.shouldBeInstanceOf() - it.declaration?.name shouldBe "TestGroup" + it.declaration?.name shouldBe "testGroup" } thirdArgumentValue.member.asClue { it.shouldNotBeNull() @@ -118,10 +120,13 @@ class GroupAnnotationProcessorTest { val parameters = testFunction.parameters parameters.shouldHaveSize(2) parameters[0] shouldBe testParameter1 - parameters[1] shouldBe parameters[1].copy( - name = "testGroup", - typeInDocs = "TestGroup" - ) + parameters[1].asClue { + it.name shouldBe "testGroup" + + val type = it.type + type.shouldBeInstanceOf() + type.declaration?.name shouldBe "TestGroup" + } } @Test @@ -159,10 +164,13 @@ class GroupAnnotationProcessorTest { val parameters = testFunction.parameters parameters.shouldHaveSize(2) parameters[0] shouldBe testParameter1 - parameters[1] shouldBe parameters[1].copy( - name = "testGroup", - typeInDocs = "TestGroup" - ) + parameters[1].asClue { + it.name shouldBe "testGroup" + + val type = it.type + type.shouldBeInstanceOf() + type.declaration?.name shouldBe "TestGroup" + } } @Test diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt index 1101b9c7d..17d9e7768 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt @@ -15,6 +15,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import io.kotest.assertions.asClue import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize @@ -38,7 +39,7 @@ class ParameterAnnotationProcessorTest { name = "testMethod", parameters = listOf(testParameter), callToOriginalAPI = PythonCall( - receiver = "testModule.TestClass.testMethod", + receiver = PythonStringifiedExpression("testModule.TestClass.testMethod"), arguments = listOf( PythonArgument( value = PythonReference(testParameter) @@ -88,10 +89,10 @@ class ParameterAnnotationProcessorTest { val attributes = testClass.attributes attributes.shouldHaveSize(1) - attributes[0] shouldBe attributes[0].copy( - name = "testParameter", - value = "true" - ) + attributes[0].asClue { + it.name shouldBe "testParameter" + it.value shouldBe PythonStringifiedExpression("True") + } } @Test @@ -158,7 +159,7 @@ class ParameterAnnotationProcessorTest { testPackage.processParameterAnnotations() testParameter.assignedBy shouldBe PythonParameterAssignment.NAME_ONLY - testParameter.defaultValue shouldBe "true" + testParameter.defaultValue shouldBe PythonStringifiedExpression("True") } @Test @@ -174,7 +175,7 @@ class ParameterAnnotationProcessorTest { @Test fun `should process RequiredAnnotations`() { - testParameter.defaultValue = "true" + testParameter.defaultValue = PythonStringifiedExpression("true") testParameter.annotations += RequiredAnnotation testPackage.processParameterAnnotations() @@ -185,7 +186,7 @@ class ParameterAnnotationProcessorTest { @Test fun `should remove RequiredAnnotations`() { - testParameter.defaultValue = "true" + testParameter.defaultValue = PythonStringifiedExpression("true") testParameter.annotations += RequiredAnnotation testPackage.processParameterAnnotations() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt index 3437fefef..8964c88e2 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt @@ -11,6 +11,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import io.kotest.assertions.asClue import io.kotest.matchers.collections.exist import io.kotest.matchers.collections.shouldBeEmpty @@ -30,6 +31,7 @@ import org.junit.jupiter.api.Test class PostprocessorTest { private lateinit var testFunction: PythonFunction + private lateinit var testConstructor: PythonConstructor private lateinit var testConstructorParameter: PythonParameter private lateinit var testClass: PythonClass private lateinit var testModule: PythonModule @@ -38,27 +40,28 @@ class PostprocessorTest { @BeforeEach fun reset() { testFunction = PythonFunction(name = "testFunction") + testConstructor = PythonConstructor( + parameters = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ) + ) + ) testConstructorParameter = PythonParameter(name = "constructorParameter") testClass = PythonClass( name = "TestClass", - constructor = PythonConstructor( - parameters = listOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - PythonParameter( - name = "positionOrName", - assignedBy = PythonParameterAssignment.POSITION_OR_NAME - ) - ) - ), + constructor = testConstructor, methods = listOf( PythonFunction( name = "__init__", parameters = listOf(testConstructorParameter), callToOriginalAPI = PythonCall( - receiver = "testModule.TestClass.__init__", + receiver = PythonStringifiedExpression("testModule.TestClass.__init__"), arguments = listOf( PythonArgument(value = PythonReference(testConstructorParameter)) ) @@ -105,7 +108,44 @@ class PostprocessorTest { inner class ReorderParameters { @Test - fun `should reorder parameters`() { + fun `should reorder parameters of constructors`() { + val implicit = PythonParameter( + name = "implicit", + assignedBy = PythonParameterAssignment.IMPLICIT + ) + val positionOnly = PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ) + val positionOrName = PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ) + val nameOnly = PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + + testConstructor.parameters.clear() + testConstructor.parameters += listOf( + nameOnly, + positionOrName, + positionOnly, + implicit + ) + + testPackage.reorderParameters() + + testConstructor.parameters.shouldContainExactly( + implicit, + positionOnly, + positionOrName, + nameOnly + ) + } + + @Test + fun `should reorder parameters of functions`() { val implicit = PythonParameter( name = "implicit", assignedBy = PythonParameterAssignment.IMPLICIT @@ -133,12 +173,10 @@ class PostprocessorTest { testPackage.reorderParameters() testFunction.parameters.shouldContainExactly( - listOf( - implicit, - positionOnly, - positionOrName, - nameOnly - ) + implicit, + positionOnly, + positionOrName, + nameOnly ) } } @@ -158,7 +196,7 @@ class PostprocessorTest { it.parameters.shouldBeEmpty() val callToOriginalAPI = it.callToOriginalAPI.shouldNotBeNull() - callToOriginalAPI.receiver shouldBe "testModule.TestClass" + callToOriginalAPI.receiver shouldBe PythonStringifiedExpression("testModule.TestClass") callToOriginalAPI.arguments.shouldBeEmpty() } } @@ -187,7 +225,7 @@ class PostprocessorTest { .callToOriginalAPI .asClue { it.shouldNotBeNull() - it.receiver shouldBe "testModule.TestClass" + it.receiver shouldBe PythonStringifiedExpression("testModule.TestClass") it.arguments.shouldHaveSize(1) val argument = it.arguments[0] diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt index 114ed3370..6fb234add 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt @@ -9,6 +9,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import io.kotest.assertions.asClue import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContain @@ -42,7 +43,7 @@ class PreprocessorTest { ) testOptionalParameter = PythonParameter( name = "testOptionalParameter", - defaultValue = "'value'", + defaultValue = PythonStringifiedExpression("'value'"), assignedBy = PythonParameterAssignment.POSITION_OR_NAME ) testGlobalFunction = PythonFunction( @@ -162,7 +163,7 @@ class PreprocessorTest { testPackage.addOriginalDeclarations() val callToOriginalAPI = testGlobalFunction.callToOriginalAPI.shouldNotBeNull() - callToOriginalAPI.receiver shouldBe "testModule.testGlobalFunction" + callToOriginalAPI.receiver shouldBe PythonStringifiedExpression("testModule.testGlobalFunction") val arguments = callToOriginalAPI.arguments arguments.shouldHaveSize(2) @@ -185,8 +186,7 @@ class PreprocessorTest { testPackage.addOriginalDeclarations() val callToOriginalAPI = testMethod.callToOriginalAPI.shouldNotBeNull() - callToOriginalAPI.receiver shouldBe "self.instance.testMethod" - + callToOriginalAPI.receiver shouldBe PythonStringifiedExpression("self.instance.testMethod") callToOriginalAPI.arguments.shouldHaveSize(0) } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt index 697631522..bdf9feb37 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt @@ -1,5 +1,6 @@ package com.larsreimann.api_editor.transformation +import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonFunction @@ -7,6 +8,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -15,6 +17,7 @@ class RenameAnnotationProcessorTest { private lateinit var testClass: PythonClass private lateinit var testFunction: PythonFunction private lateinit var testParameter: PythonParameter + private lateinit var testGroupAnnotation: GroupAnnotation private lateinit var testPackage: PythonPackage @BeforeEach @@ -25,13 +28,18 @@ class RenameAnnotationProcessorTest { ) testFunction = PythonFunction( name = "testFunction", - annotations = mutableListOf(RenameAnnotation("newTestFunction")) + annotations = mutableListOf( + RenameAnnotation("newTestFunction") + ) ) testParameter = PythonParameter( name = "testParameter", annotations = mutableListOf(RenameAnnotation("newTestParameter")) ) - + testGroupAnnotation = GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter") + ) testPackage = PythonPackage( distribution = "testPackage", name = "testPackage", @@ -44,7 +52,8 @@ class RenameAnnotationProcessorTest { testFunction, PythonFunction( name = "testFunction", - parameters = listOf(testParameter) + parameters = listOf(testParameter), + annotations = mutableListOf(testGroupAnnotation) ) ) ) @@ -91,6 +100,13 @@ class RenameAnnotationProcessorTest { testParameter.name shouldBe "newTestParameter" } + @Test + fun `should update GroupAnnotation on containing function`() { + testPackage.processRenameAnnotations() + + testGroupAnnotation.parameters.shouldContainExactly("newTestParameter") + } + @Test fun `should remove RenameAnnotations of parameters`() { testPackage.processRenameAnnotations() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt index b97510346..da337d85c 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt @@ -14,6 +14,7 @@ import com.larsreimann.api_editor.model.PythonFromImport import com.larsreimann.api_editor.model.PythonImport import com.larsreimann.api_editor.model.PythonParameterAssignment import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.model.SerializablePythonClass import com.larsreimann.api_editor.model.SerializablePythonFunction import com.larsreimann.api_editor.model.SerializablePythonModule @@ -223,10 +224,8 @@ internal class AnnotationValidatorTest { "typeInDocs", "description", mutableListOf( - AttributeAnnotation( - DefaultString("test") - ), RenameAnnotation("newName"), + RequiredAnnotation, BoundaryAnnotation( false, 1.0, @@ -302,7 +301,7 @@ internal class AnnotationValidatorTest { CalledAfterAnnotation("calledAfterName2"), GroupAnnotation( "groupName", - listOf("test-module.test-function.test-parameter") + mutableListOf("test-module.test-function.test-parameter") ), RenameAnnotation("newName") ) @@ -549,7 +548,7 @@ internal class AnnotationValidatorTest { mutableListOf( GroupAnnotation( "paramGroup", - listOf("first-param", "second-param") + mutableListOf("first-param", "second-param") ) ) ), @@ -592,7 +591,7 @@ internal class AnnotationValidatorTest { mutableListOf( GroupAnnotation( "paramGroup", - listOf("second-param") + mutableListOf("second-param") ) ) ) @@ -642,7 +641,7 @@ internal class AnnotationValidatorTest { mutableListOf( GroupAnnotation( "paramGroup", - listOf("second-param") + mutableListOf("second-param") ) ) )