diff --git a/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc b/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc index 7e41ced51e..8240425e53 100644 --- a/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc +++ b/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc @@ -100,7 +100,12 @@ Inside of the `metamodel` block the following settings can be configured. |`registrationHelperName` |String -|Name of the registration helper +|Fully qualified name of the generated language registration helper + +|`conceptPropertiesInterfaceName` +|String +|Fully qualified name of the generated interface, that contains the concept meta-properties of this language set. +If `null` (default), neither the concept meta-properties nor the corresponding interface will be generated. |`taskDependencies` |List diff --git a/metamodel-export/org.modelix.metamodel.export/models/org.modelix.metamodel.export.mps b/metamodel-export/org.modelix.metamodel.export/models/org.modelix.metamodel.export.mps index a9aeb8d905..ea1527c01f 100644 --- a/metamodel-export/org.modelix.metamodel.export/models/org.modelix.metamodel.export.mps +++ b/metamodel-export/org.modelix.metamodel.export/models/org.modelix.metamodel.export.mps @@ -1748,11 +1748,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1799,6 +1858,9 @@ + + + diff --git a/model-api-gen-gradle-test/build.gradle.kts b/model-api-gen-gradle-test/build.gradle.kts index a8b4099ee9..ec9d6cd522 100644 --- a/model-api-gen-gradle-test/build.gradle.kts +++ b/model-api-gen-gradle-test/build.gradle.kts @@ -104,6 +104,7 @@ metamodel { typedNodeImpl.suffix = "Impl" } registrationHelperName = "org.modelix.apigen.test.ApigenTestLanguages" + conceptPropertiesInterfaceName = "org.modelix.apigen.test.IMetaConceptProperties" } node { diff --git a/model-api-gen-gradle-test/src/test/kotlin/GeneratedApiTest.kt b/model-api-gen-gradle-test/src/test/kotlin/org/modelix/metamodel/generator/GeneratedApiTest.kt similarity index 77% rename from model-api-gen-gradle-test/src/test/kotlin/GeneratedApiTest.kt rename to model-api-gen-gradle-test/src/test/kotlin/org/modelix/metamodel/generator/GeneratedApiTest.kt index d3fd445925..5ce0934e64 100644 --- a/model-api-gen-gradle-test/src/test/kotlin/GeneratedApiTest.kt +++ b/model-api-gen-gradle-test/src/test/kotlin/org/modelix/metamodel/generator/GeneratedApiTest.kt @@ -4,10 +4,12 @@ import jetbrains.mps.baseLanguage.jdk8.C_SuperInterfaceMethodCall_old import jetbrains.mps.baseLanguage.jdk8.SuperInterfaceMethodCall_old import jetbrains.mps.lang.behavior.C_ConceptMethodDeclaration import jetbrains.mps.lang.behavior.ConceptMethodDeclaration +import jetbrains.mps.lang.core.C_BaseConcept import jetbrains.mps.lang.core.L_jetbrains_mps_lang_core import jetbrains.mps.lang.editor.C_FontStyleStyleClassItem import jetbrains.mps.lang.editor.L_jetbrains_mps_lang_editor import jetbrains.mps.lang.editor._FontStyle_Enum +import org.modelix.apigen.test.IMetaConceptProperties import org.modelix.metamodel.IPropertyValueEnum import org.modelix.metamodel.TypedLanguagesRegistry import org.modelix.metamodel.typed @@ -15,6 +17,7 @@ import org.modelix.metamodel.untyped import org.modelix.model.ModelFacade import org.modelix.model.api.INode import org.modelix.model.api.getRootNode +import org.modelix.model.data.ConceptData import org.modelix.model.data.ModelData import java.io.File import kotlin.reflect.KAnnotatedElement @@ -23,6 +26,8 @@ import kotlin.reflect.full.isSubclassOf import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue class GeneratedApiTest { @@ -35,7 +40,7 @@ class GeneratedApiTest { branch.runWrite { data.load(branch) val node = findNodeWithStyleAttribute(branch.getRootNode())!!.typed(C_FontStyleStyleClassItem.getInstanceInterface()) - assert(_FontStyle_Enum::class.isSubclassOf(IPropertyValueEnum::class)) + assertTrue(_FontStyle_Enum::class.isSubclassOf(IPropertyValueEnum::class)) assertContains(_FontStyle_Enum.values(), node.style) val enumValue = _FontStyle_Enum.BOLD_ITALIC node.style = enumValue @@ -55,15 +60,28 @@ class GeneratedApiTest { val foundDeprecatedNodeChildLink = ClassConcept::class.members.any { it.hasDeprecationWithMessage() } val foundDeprecatedNodeReference = SuperInterfaceMethodCall_old::class.members.any { it.hasDeprecationWithMessage() } - assert(foundDeprecatedConcept) - assert(foundDeprecatedProperty) - assert(foundDeprecatedChildLink) - assert(foundDeprecatedReference) + assertTrue(foundDeprecatedConcept) + assertTrue(foundDeprecatedProperty) + assertTrue(foundDeprecatedChildLink) + assertTrue(foundDeprecatedReference) - assert(foundDeprecatedNodeWrapper) - assert(foundDeprecatedNodeProperty) - assert(foundDeprecatedNodeChildLink) - assert(foundDeprecatedNodeReference) + assertTrue(foundDeprecatedNodeWrapper) + assertTrue(foundDeprecatedNodeProperty) + assertTrue(foundDeprecatedNodeChildLink) + assertTrue(foundDeprecatedNodeReference) + } + + @Test + fun `metaProperty alias is generated`() { + val hasAlias = IMetaConceptProperties::class.members.any { it.name == ConceptData.ALIAS_KEY } + assertTrue(hasAlias) + assertNull(C_BaseConcept.alias) + } + + @Test + fun `metaProperty alias has value`() { + val alias = C_ClassConcept.alias + assertEquals("class", alias) } private fun KAnnotatedElement.hasDeprecationWithMessage() = diff --git a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateMetaModelSources.kt b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateMetaModelSources.kt index 5661d944b6..8499669ef5 100644 --- a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateMetaModelSources.kt +++ b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateMetaModelSources.kt @@ -52,6 +52,10 @@ abstract class GenerateMetaModelSources @Inject constructor(of: ObjectFactory) : @Optional val registrationHelperName: Property = of.property(String::class.java) + @get:Input + @Optional + val conceptPropertiesInterfaceName: Property = of.property(String::class.java) + @get: Input val nameConfig: Property = of.property(NameConfig::class.java) @@ -100,6 +104,7 @@ abstract class GenerateMetaModelSources @Inject constructor(of: ObjectFactory) : kotlinOutputDir.toPath(), nameConfig.get(), this.modelqlKotlinOutputDir.orNull?.asFile?.toPath(), + conceptPropertiesInterfaceName.orNull, ) generator.generate(processedLanguages) registrationHelperName.orNull?.let { diff --git a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradlePlugin.kt b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradlePlugin.kt index 3a8154e709..4a75140a95 100644 --- a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradlePlugin.kt +++ b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradlePlugin.kt @@ -98,6 +98,7 @@ class MetaModelGradlePlugin : Plugin { task.exportedLanguagesDir.set(exportedLanguagesDir) } settings.registrationHelperName?.let { task.registrationHelperName.set(it) } + settings.conceptPropertiesInterfaceName?.let { task.conceptPropertiesInterfaceName.set(it) } task.nameConfig.set(settings.nameConfig) } } diff --git a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradleSettings.kt b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradleSettings.kt index 067f5fcba6..cb7ad07d64 100644 --- a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradleSettings.kt +++ b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/MetaModelGradleSettings.kt @@ -30,6 +30,7 @@ open class MetaModelGradleSettings { } var typescriptDir: File? = null var registrationHelperName: String? = null + var conceptPropertiesInterfaceName: String? = null val taskDependencies: MutableList = ArrayList() internal val nameConfig = NameConfig() diff --git a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/GeneratorInput.kt b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/GeneratorInput.kt index ad6d5f0770..0c56199d6c 100644 --- a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/GeneratorInput.kt +++ b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/GeneratorInput.kt @@ -36,6 +36,7 @@ internal class ProcessedLanguageSet(dataList: List) : IProcessedLa private lateinit var uid2language: Map private lateinit var fqName2concept: Map private lateinit var uid2concept: Map + private lateinit var conceptMetaProperties: MutableSet init { load(dataList) @@ -62,6 +63,14 @@ internal class ProcessedLanguageSet(dataList: List) : IProcessedLa initIndexes() resolveConceptReferences() fixRoleConflicts() + collectConceptMetaProperties() + } + + private fun collectConceptMetaProperties() { + conceptMetaProperties = mutableSetOf() + val concepts = languages.flatMap { it.getConcepts() } + val keys = concepts.flatMap { it.metaProperties.keys }.toSet() + conceptMetaProperties.addAll(keys) } private fun initIndexes() { @@ -137,6 +146,8 @@ internal class ProcessedLanguageSet(dataList: List) : IProcessedLa fun getLanguages(): List { return languages } + + fun getConceptMetaProperties() = conceptMetaProperties } internal class ProcessedLanguage(var name: String, var uid: String?) { @@ -162,7 +173,14 @@ internal class ProcessedLanguage(var name: String, var uid: String?) { fun load(dataList: List) { for (data in dataList) { addConcept( - ProcessedConcept(data.name, data.uid, data.abstract, data.extends.map { ProcessedConceptReference(it) }.toMutableList(), data.deprecationMessage).also { concept -> + ProcessedConcept( + data.name, + data.uid, + data.abstract, + data.extends.map { ProcessedConceptReference(it) }.toMutableList(), + data.deprecationMessage, + data.metaProperties, + ).also { concept -> concept.loadRoles(data) }, ) @@ -220,6 +238,7 @@ internal class ProcessedConcept( var abstract: Boolean, val extends: MutableList, override var deprecationMessage: String?, + val metaProperties: MutableMap, ) : IProcessedDeprecatable { lateinit var language: ProcessedLanguage private val roles: MutableList = ArrayList() diff --git a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/MetaModelGenerator.kt b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/MetaModelGenerator.kt index 6b075705de..c625dda83b 100644 --- a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/MetaModelGenerator.kt +++ b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/MetaModelGenerator.kt @@ -56,7 +56,12 @@ import org.modelix.modelql.typed.TypedModelQL import java.nio.file.Path import kotlin.reflect.KClass -class MetaModelGenerator(val outputDir: Path, val nameConfig: NameConfig = NameConfig(), val modelqlOutputDir: Path? = null) { +class MetaModelGenerator( + val outputDir: Path, + val nameConfig: NameConfig = NameConfig(), + val modelqlOutputDir: Path? = null, + val conceptPropertiesInterfaceName: String? = null, +) { var alwaysUseNonNullableProperties: Boolean = true private val headerComment = "\ngenerated by modelix model-api-gen \n" @@ -97,6 +102,7 @@ class MetaModelGenerator(val outputDir: Path, val nameConfig: NameConfig = NameC } internal fun generateRegistrationHelper(classFqName: String, languages: ProcessedLanguageSet) { + require(classFqName.contains(".")) { "The name of the registrationHelper does not contain a dot. Use a fully qualified name." } val typeName = ClassName(classFqName.substringBeforeLast("."), classFqName.substringAfterLast(".")) val cls = TypeSpec.objectBuilder(typeName) .addProperty( @@ -117,11 +123,42 @@ class MetaModelGenerator(val outputDir: Path, val nameConfig: NameConfig = NameC .write() } + private fun generateConceptMetaPropertiesInterface(languages: IProcessedLanguageSet) { + val fqName = checkNotNull(conceptPropertiesInterfaceName) + require(fqName.contains(".")) { "The name of the concept properties interface does not contain a dot. Use a fully qualified name." } + val interfaceName = ClassName(fqName.substringBeforeLast("."), fqName.substringAfterLast(".")) + val metaPropertiesInterface = TypeSpec.interfaceBuilder(interfaceName) + .generateMetaProperties(languages as ProcessedLanguageSet) + .build() + + FileSpec.builder(interfaceName.packageName, interfaceName.simpleName) + .addFileComment(headerComment) + .addType(metaPropertiesInterface) + .build() + .write() + } + + private fun TypeSpec.Builder.generateMetaProperties(languages: ProcessedLanguageSet): TypeSpec.Builder { + val nullGetter = FunSpec.getterBuilder().addCode("return null").build() + languages.getConceptMetaProperties().forEach { + addProperty( + PropertySpec.builder(it, String::class.asTypeName().copy(nullable = true)) + .getter(nullGetter) + .build(), + ) + } + return this + } + fun generate(languages: IProcessedLanguageSet) { generate(languages as ProcessedLanguageSet) } private fun generate(languages: ProcessedLanguageSet) { + if (conceptPropertiesInterfaceName != null) { + generateConceptMetaPropertiesInterface(languages) + } + for (language in languages.getLanguages()) { language.packageDir().toFile().listFiles()?.filter { it.isFile }?.forEach { it.delete() } val builder = @@ -647,6 +684,13 @@ class MetaModelGenerator(val outputDir: Path, val nameConfig: NameConfig = NameC for (extended in concept.getDirectSuperConcepts()) { addSuperinterface(extended.conceptWrapperInterfaceClass().parameterizedBy(nodeT)) } + + if (conceptPropertiesInterfaceName != null && concept.extends.isEmpty()) { + val pckgName = conceptPropertiesInterfaceName.substringBeforeLast(".") + val interfaceName = conceptPropertiesInterfaceName.substringAfterLast(".") + addSuperinterface(ClassName(pckgName, interfaceName)) + } + for (feature in concept.getOwnRoles()) { when (feature) { is ProcessedProperty -> addProperty( @@ -694,6 +738,16 @@ class MetaModelGenerator(val outputDir: Path, val nameConfig: NameConfig = NameC .addStatement("return %T", concept.conceptObjectType()) .build(), ) + if (conceptPropertiesInterfaceName != null) { + concept.metaProperties.forEach { (key, value) -> + addProperty( + PropertySpec.builder(key, String::class.asTypeName()) + .addModifiers(KModifier.OVERRIDE) + .initializer("%S", value) + .build(), + ) + } + } }.build(), ) }.build() diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/data/MetaModelData.kt b/model-api/src/commonMain/kotlin/org/modelix/model/data/MetaModelData.kt index 2bd9922c1f..df4a11a140 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/data/MetaModelData.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/data/MetaModelData.kt @@ -43,7 +43,12 @@ data class ConceptData( val references: List = emptyList(), val extends: List = emptyList(), override val deprecationMessage: String? = null, -) : IDeprecatable + val metaProperties: MutableMap = mutableMapOf(), +) : IDeprecatable { + companion object { + const val ALIAS_KEY = "alias" + } +} @Serializable data class EnumData( diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/data/ModelData.kt b/model-api/src/commonMain/kotlin/org/modelix/model/data/ModelData.kt index 7821b5844e..744c727807 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/data/ModelData.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/data/ModelData.kt @@ -86,7 +86,10 @@ data class NodeData( val references: Map = emptyMap(), ) { companion object { - const val idPropertyKey = "#mpsNodeId#" + const val ID_PROPERTY_KEY = "#mpsNodeId#" + + @Deprecated("Use ID_PROPERTY_KEY", replaceWith = ReplaceWith("ID_PROPERTY_KEY")) + const val idPropertyKey = ID_PROPERTY_KEY } }