Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MODELIX-667 Refactor MetaModelGenerator #392

Merged
merged 13 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2024.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.modelix.metamodel.generator

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import org.modelix.metamodel.IPropertyValueEnum
import java.nio.file.Path

internal class EnumFileGenerator(private val enum: ProcessedEnum, private val outputDir: Path) {
languitar marked this conversation as resolved.
Show resolved Hide resolved

private val enumType = ClassName(enum.language.name, enum.name)

fun generateFile() {
buildEnumFileSpec().writeTo(outputDir)
}

private fun buildEnumFileSpec(): FileSpec {
val generatedEnum = TypeSpec.enumBuilder(enum.name).runBuild {
addDeprecationIfNecessary(enum)
addPrimaryConstructor()
addSuperinterface(IPropertyValueEnum::class)
addEnumProperties()
addEnumMembers()
addCompanionObject()
}

return FileSpec.builder(enum.language.name, enum.name).runBuild {
addFileComment(MetaModelGenerator.HEADER_COMMENT)
addType(generatedEnum)
}
}

private fun TypeSpec.Builder.addEnumProperties() {
val presentation = PropertySpec.builder("presentation", String::class.asTypeName().copy(nullable = true)).runBuild {
initializer("presentation")
addModifiers(KModifier.OVERRIDE)
}
val uid = PropertySpec.builder("uid", String::class).runBuild {
initializer("uid")
}

addProperty(presentation)
addProperty(uid)
}

private fun TypeSpec.Builder.addEnumMembers() {
enum.getAllMembers().forEach { addEnumMember(it) }
}

private fun TypeSpec.Builder.addEnumMember(member: ProcessedEnumMember) {
val typeSpec = TypeSpec.anonymousClassBuilder().runBuild {
addSuperclassConstructorParameter("%S", member.uid)
addSuperclassConstructorParameter(
if (member.presentation == null) "null" else "%S",
member.presentation ?: "",
)
}
addEnumConstant(member.name, typeSpec)
}

private fun TypeSpec.Builder.addCompanionObject() {
val companion = TypeSpec.companionObjectBuilder().runBuild {
addDefaultValueFun()
addGetLiteralByMemberIdFun()
}
addType(companion)
}

private fun TypeSpec.Builder.addDefaultValueFun() {
val defaultValue = FunSpec.builder("defaultValue").runBuild {
returns(enumType)
addCode("return values()[%L]", enum.defaultIndex)
}
addFunction(defaultValue)
}

private fun TypeSpec.Builder.addGetLiteralByMemberIdFun() {
val body = CodeBlock.builder().runBuild {
beginControlFlow("return when (uid) {")
for (member in enum.getAllMembers()) {
addStatement("%S -> %N", member.uid, member.name)
}
addStatement("else -> defaultValue()")
endControlFlow()
}

val getLiteralMemberByMemberId = FunSpec.builder("getLiteralByMemberId").runBuild {
returns(enumType)
addParameter("uid", String::class)
addCode(body)
}
addFunction(getLiteralMemberByMemberId)
}

private fun TypeSpec.Builder.addPrimaryConstructor() {
val constructorSpec = FunSpec.constructorBuilder().runBuild {
addParameter("uid", String::class)
addParameter("presentation", String::class.asTypeName().copy(nullable = true))
}
primaryConstructor(constructorSpec)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import org.modelix.metamodel.GeneratedReferenceLink
import org.modelix.metamodel.GeneratedSingleChildLink
import org.modelix.metamodel.IConceptOfTypedNode
import org.modelix.metamodel.INonAbstractConcept
import org.modelix.metamodel.IPropertyValueEnum
import org.modelix.metamodel.ITypedConcept
import org.modelix.metamodel.ITypedNode
import org.modelix.metamodel.MandatoryBooleanPropertySerializer
Expand Down Expand Up @@ -65,7 +64,9 @@ class MetaModelGenerator(
) {
var alwaysUseNonNullableProperties: Boolean = true

private val headerComment = "\ngenerated by modelix model-api-gen \n"
companion object {
const val HEADER_COMMENT = "\ngenerated by modelix model-api-gen \n"
}
languitar marked this conversation as resolved.
Show resolved Hide resolved

private fun ProcessedProperty.asKotlinType(): TypeName {
val nonNullableType = when (type) {
Expand Down Expand Up @@ -118,7 +119,7 @@ class MetaModelGenerator(
.build()

FileSpec.builder(typeName.packageName, typeName.simpleName)
.addFileComment(headerComment)
.addFileComment(HEADER_COMMENT)
.addType(cls)
.build()
.write()
Expand All @@ -133,7 +134,7 @@ class MetaModelGenerator(
.build()

FileSpec.builder(interfaceName.packageName, interfaceName.simpleName)
.addFileComment(headerComment)
.addFileComment(HEADER_COMMENT)
.addType(metaPropertiesInterface)
.build()
.write()
Expand Down Expand Up @@ -164,12 +165,12 @@ class MetaModelGenerator(
language.packageDir().toFile().listFiles()?.filter { it.isFile }?.forEach { it.delete() }
val builder =
FileSpec.builder(language.generatedClassName().packageName, language.generatedClassName().simpleName)
val file = builder.addFileComment(headerComment)
val file = builder.addFileComment(HEADER_COMMENT)
.addType(generateLanguage(language)).build()
file.write()

for (enum in language.getEnums()) {
generateEnumFile(enum)
EnumFileGenerator(enum, outputDir).generateFile()
}

for (concept in language.getConcepts()) {
Expand Down Expand Up @@ -204,79 +205,9 @@ class MetaModelGenerator(
return builder.build()
}

private fun generateEnumFile(enum: ProcessedEnum) {
val constructorSpec = FunSpec.constructorBuilder()
.addParameter("uid", String::class)
.addParameter("presentation", String::class.asTypeName().copy(nullable = true))
.build()

val enumBuilder = TypeSpec.enumBuilder(enum.name)
.addDeprecationIfNecessary(enum)
.primaryConstructor(constructorSpec)
.addSuperinterface(IPropertyValueEnum::class)
.addProperty(
PropertySpec.builder("uid", String::class)
.initializer("uid")
.build(),
)
.addProperty(
PropertySpec.builder("presentation", String::class.asTypeName().copy(nullable = true))
.initializer("presentation")
.addModifiers(KModifier.OVERRIDE)
.build(),
)

val enumType = ClassName(enum.language.name, enum.name)
val getLiteralFunBuilder = FunSpec.builder("getLiteralByMemberId")
.returns(enumType)
.addParameter("uid", String::class)
val getLiteralCodeBuilder = CodeBlock.builder().beginControlFlow("return when (uid) {")

for (member in enum.getAllMembers()) {
enumBuilder.addEnumConstant(
member.name,
TypeSpec.anonymousClassBuilder()
.addSuperclassConstructorParameter("%S", member.uid)
.addSuperclassConstructorParameter(
if (member.presentation == null) "null" else "%S",
member.presentation ?: "",
)
.build(),
)
getLiteralCodeBuilder.addStatement("%S -> %N", member.uid, member.name)
}

getLiteralFunBuilder.addCode(
getLiteralCodeBuilder
.addStatement("else -> defaultValue()")
.endControlFlow()
.build(),
)

val companion = TypeSpec.companionObjectBuilder()
.addFunction(
FunSpec.builder("defaultValue")
.returns(enumType)
.addCode("return values()[%L]", enum.defaultIndex)
.build(),
)
.addFunction(
getLiteralFunBuilder.build(),
)
.build()

val generatedEnum = enumBuilder.addType(companion).build()

FileSpec.builder(enum.language.name, enum.name)
.addFileComment(headerComment)
.addType(generatedEnum)
.build()
.write()
}

private fun generateConceptFile(concept: ProcessedConcept) {
FileSpec.builder(concept.language.name, concept.name)
.addFileComment(headerComment)
.addFileComment(HEADER_COMMENT)
.addType(generateConceptObject(concept))
.addTypeAlias(TypeAliasSpec.builder(concept.conceptTypeAliasName(), concept.conceptWrapperInterfaceType()).build())
.addType(generateConceptWrapperInterface(concept))
Expand Down Expand Up @@ -383,7 +314,7 @@ class MetaModelGenerator(

private fun generateModelQLFile(concept: ProcessedConcept) {
FileSpec.builder("org.modelix.modelql.gen." + concept.language.name, concept.name)
.addFileComment(headerComment)
.addFileComment(HEADER_COMMENT)
.apply {
for (feature in concept.getOwnRoles()) {
val receiverType = Iterable::class.asTypeName().parameterizedBy(concept.nodeWrapperInterfaceType())
Expand Down Expand Up @@ -1046,20 +977,6 @@ class MetaModelGenerator(
}.build()
}

private fun generateDeprecationAnnotation(message: String): AnnotationSpec {
val annotationBuilder = AnnotationSpec.builder(Deprecated::class)
if (message.isNotEmpty()) { annotationBuilder.addMember("message = %S", message) }
return annotationBuilder.build()
}

private fun TypeSpec.Builder.addDeprecationIfNecessary(deprecatable: IProcessedDeprecatable): TypeSpec.Builder {
return deprecatable.deprecationMessage?.let { addAnnotation(generateDeprecationAnnotation(it)) } ?: this
}

private fun PropertySpec.Builder.addDeprecationIfNecessary(deprecatable: IProcessedDeprecatable): PropertySpec.Builder {
return deprecatable.deprecationMessage?.let { addAnnotation(generateDeprecationAnnotation(it)) } ?: this
}

private fun ProcessedConcept.conceptWrapperInterfaceType() =
conceptWrapperInterfaceClass().parameterizedBy(nodeWrapperInterfaceType())

Expand Down Expand Up @@ -1117,3 +1034,42 @@ private fun List<TypeName>.toListLiteralCodeBlock(): CodeBlock {
add(")")
}.build()
}

internal fun generateDeprecationAnnotation(message: String): AnnotationSpec {
val annotationBuilder = AnnotationSpec.builder(Deprecated::class)
if (message.isNotEmpty()) { annotationBuilder.addMember("message = %S", message) }
return annotationBuilder.build()
}

internal fun TypeSpec.Builder.addDeprecationIfNecessary(deprecatable: IProcessedDeprecatable): TypeSpec.Builder {
return deprecatable.deprecationMessage?.let { addAnnotation(generateDeprecationAnnotation(it)) } ?: this
}

internal fun PropertySpec.Builder.addDeprecationIfNecessary(deprecatable: IProcessedDeprecatable): PropertySpec.Builder {
return deprecatable.deprecationMessage?.let { addAnnotation(generateDeprecationAnnotation(it)) } ?: this
}

internal inline fun FunSpec.Builder.runBuild(crossinline body: FunSpec.Builder.() -> Unit): FunSpec {
body()
return build()
}

internal inline fun TypeSpec.Builder.runBuild(crossinline body: TypeSpec.Builder.() -> Unit): TypeSpec {
body()
return build()
}

internal inline fun FileSpec.Builder.runBuild(crossinline body: FileSpec.Builder.() -> Unit): FileSpec {
body()
return build()
}

internal inline fun PropertySpec.Builder.runBuild(crossinline body: PropertySpec.Builder.() -> Unit): PropertySpec {
body()
return build()
}

internal inline fun CodeBlock.Builder.runBuild(crossinline body: CodeBlock.Builder.() -> Unit): CodeBlock {
body()
return build()
}