diff --git a/build.gradle.kts b/build.gradle.kts index 205f0313..e33109fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { kover(project(":semantic")) kover(project(":cli")) kover(project(":language-server")) + kover(project(":samt-config")) } koverReport { diff --git a/common/src/main/kotlin/tools/samt/common/SamtConfiguration.kt b/common/src/main/kotlin/tools/samt/common/SamtConfiguration.kt new file mode 100644 index 00000000..6ec74f5d --- /dev/null +++ b/common/src/main/kotlin/tools/samt/common/SamtConfiguration.kt @@ -0,0 +1,26 @@ +package tools.samt.common + +data class SamtConfiguration( + val source: String, + val plugins: List, + val generators: List, +) + +sealed interface SamtPluginConfiguration + +data class SamtLocalPluginConfiguration( + val path: String, +) : SamtPluginConfiguration + +data class SamtMavenPluginConfiguration( + val groupId: String, + val artifactId: String, + val version: String, + val repository: String, +) : SamtPluginConfiguration + +data class SamtGeneratorConfiguration( + val name: String, + val output: String, + val options: Map, +) diff --git a/common/src/main/kotlin/tools/samt/common/SamtLinterConfiguration.kt b/common/src/main/kotlin/tools/samt/common/SamtLinterConfiguration.kt new file mode 100644 index 00000000..414d251e --- /dev/null +++ b/common/src/main/kotlin/tools/samt/common/SamtLinterConfiguration.kt @@ -0,0 +1,37 @@ +package tools.samt.common + +data class SamtLinterConfiguration( + val splitModelAndProviders: SplitModelAndProvidersConfiguration, + val namingConventions: NamingConventionsConfiguration, +) + +sealed interface SamtRuleConfiguration { + val level: DiagnosticSeverity? +} + +data class SplitModelAndProvidersConfiguration( + override val level: DiagnosticSeverity?, +) : SamtRuleConfiguration + +data class NamingConventionsConfiguration( + override val level: DiagnosticSeverity?, + val record: NamingConvention, + val recordField: NamingConvention, + val enum: NamingConvention, + val enumValue: NamingConvention, + val typeAlias: NamingConvention, + val service: NamingConvention, + val serviceOperation: NamingConvention, + val serviceOperationParameter: NamingConvention, + val provider: NamingConvention, + val samtPackage: NamingConvention, + val fileName: NamingConvention, +) : SamtRuleConfiguration { + enum class NamingConvention { + PascalCase, + CamelCase, + SnakeCase, + KebabCase, + ScreamingSnakeCase, + } +} diff --git a/samt-config/build.gradle.kts b/samt-config/build.gradle.kts new file mode 100644 index 00000000..05dd071c --- /dev/null +++ b/samt-config/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("samt-core.kotlin-conventions") + alias(libs.plugins.kover) +} + +dependencies { + implementation(project(":common")) + implementation(libs.kotlinx.serialization.yaml) +} diff --git a/samt-config/src/main/kotlin/tools/samt/config/DefaultRuleConfigurations.kt b/samt-config/src/main/kotlin/tools/samt/config/DefaultRuleConfigurations.kt new file mode 100644 index 00000000..3182ad9a --- /dev/null +++ b/samt-config/src/main/kotlin/tools/samt/config/DefaultRuleConfigurations.kt @@ -0,0 +1,29 @@ +package tools.samt.config + +import tools.samt.common.DiagnosticSeverity as CommonDiagnosticSeverity +import tools.samt.common.NamingConventionsConfiguration.NamingConvention as CommonNamingConvention + +val recommended = tools.samt.common.SamtLinterConfiguration( + splitModelAndProviders = tools.samt.common.SplitModelAndProvidersConfiguration( + level = CommonDiagnosticSeverity.Info, + ), + namingConventions = tools.samt.common.NamingConventionsConfiguration( + level = CommonDiagnosticSeverity.Warning, + record = CommonNamingConvention.PascalCase, + recordField = CommonNamingConvention.CamelCase, + enum = CommonNamingConvention.PascalCase, + enumValue = CommonNamingConvention.ScreamingSnakeCase, + typeAlias = CommonNamingConvention.PascalCase, + service = CommonNamingConvention.PascalCase, + serviceOperation = CommonNamingConvention.CamelCase, + serviceOperationParameter = CommonNamingConvention.CamelCase, + provider = CommonNamingConvention.PascalCase, + samtPackage = CommonNamingConvention.SnakeCase, + fileName = CommonNamingConvention.KebabCase, + ), +) + +val strict = recommended.copy( + splitModelAndProviders = recommended.splitModelAndProviders.copy(level = CommonDiagnosticSeverity.Warning), + namingConventions = recommended.namingConventions.copy(level = CommonDiagnosticSeverity.Error), +) diff --git a/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt b/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt new file mode 100644 index 00000000..78aa56a1 --- /dev/null +++ b/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt @@ -0,0 +1,49 @@ +package tools.samt.config + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SamtConfiguration( + val source: String = "./src", + val repositories: SamtRepositoriesConfiguration = SamtRepositoriesConfiguration(), + val plugins: List = emptyList(), + val generators: List = emptyList(), +) + +@Serializable +data class SamtRepositoriesConfiguration( + val maven: String = "https://repo.maven.apache.org/maven2" +) + +@Serializable +sealed interface SamtPluginConfiguration + +@Serializable +@SerialName("local") +data class SamtLocalPluginConfiguration( + val path: String, +) : SamtPluginConfiguration + +@Serializable +@SerialName("maven") +data class SamtMavenPluginConfiguration( + val groupId: String, + val artifactId: String, + val version: String, + val repository: String? = null, +) : SamtPluginConfiguration + +@Serializable +@SerialName("gradle") +data class SamtGradlePluginConfiguration( + val dependency: String, + val repository: String? = null, +) : SamtPluginConfiguration + +@Serializable +data class SamtGeneratorConfiguration( + val name: String, + val output: String = "./out", + val options: Map = emptyMap(), +) diff --git a/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt b/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt new file mode 100644 index 00000000..3c64f55b --- /dev/null +++ b/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt @@ -0,0 +1,119 @@ +package tools.samt.config + +import com.charleskorn.kaml.* +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.inputStream +import tools.samt.common.DiagnosticSeverity as CommonDiagnosticSeverity +import tools.samt.common.NamingConventionsConfiguration as CommonNamingConventionsConfiguration +import tools.samt.common.NamingConventionsConfiguration.NamingConvention as CommonNamingConvention +import tools.samt.common.SamtConfiguration as CommonSamtConfiguration +import tools.samt.common.SamtGeneratorConfiguration as CommonGeneratorConfiguration +import tools.samt.common.SamtLinterConfiguration as CommonLinterConfiguration +import tools.samt.common.SamtLocalPluginConfiguration as CommonLocalPluginConfiguration +import tools.samt.common.SamtMavenPluginConfiguration as CommonMavenPluginConfiguration +import tools.samt.common.SplitModelAndProvidersConfiguration as CommonSplitModelAndProvidersConfiguration + +object SamtConfigurationParser { + private val yaml = Yaml( + configuration = YamlConfiguration( + encodeDefaults = false, + polymorphismStyle = PolymorphismStyle.Property, + singleLineStringStyle = SingleLineStringStyle.Plain, + ) + ) + + fun parseConfiguration(path: Path): CommonSamtConfiguration { + val parsedConfiguration: SamtConfiguration = if (path.exists()) { + yaml.decodeFromStream(path.inputStream()) + } else { + SamtConfiguration() + } + + return CommonSamtConfiguration( + source = parsedConfiguration.source, + plugins = parsedConfiguration.plugins.map { plugin -> + when (plugin) { + is SamtLocalPluginConfiguration -> CommonLocalPluginConfiguration( + path = plugin.path + ) + + is SamtMavenPluginConfiguration -> CommonMavenPluginConfiguration( + groupId = plugin.groupId, + artifactId = plugin.artifactId, + version = plugin.version, + repository = plugin.repository ?: parsedConfiguration.repositories.maven + ) + + is SamtGradlePluginConfiguration -> CommonMavenPluginConfiguration( + groupId = plugin.dependency.split(':')[0], + artifactId = plugin.dependency.split(':')[1], + version = plugin.dependency.split(':')[2], + repository = plugin.repository ?: parsedConfiguration.repositories.maven + ) + } + }, + generators = parsedConfiguration.generators.map { generator -> + CommonGeneratorConfiguration( + name = generator.name, + output = generator.output, + options = generator.options + ) + } + ) + } + + fun parseLinterConfiguration(path: Path): CommonLinterConfiguration { + val parsedLinterConfiguration: SamtLinterConfiguration = if (path.exists()) { + yaml.decodeFromStream(path.inputStream()) + } else { + SamtLinterConfiguration() + } + + val base = when (parsedLinterConfiguration.extends) { + "recommended" -> recommended + "strict" -> strict + else -> error("TODO") + } + + val userSplitModelAndProvidersConfiguration = parsedLinterConfiguration.rules.filterIsInstance().singleOrNull() + val userNamingConventionsConfiguration = parsedLinterConfiguration.rules.filterIsInstance().singleOrNull() + + return CommonLinterConfiguration( + splitModelAndProviders = CommonSplitModelAndProvidersConfiguration( + level = userSplitModelAndProvidersConfiguration?.level.toLevelOrDefault(base.splitModelAndProviders.level), + ), + namingConventions = CommonNamingConventionsConfiguration( + level = userNamingConventionsConfiguration?.level.toLevelOrDefault(base.namingConventions.level), + record = userNamingConventionsConfiguration?.record.toNamingConventionOrDefault(base.namingConventions.record), + recordField = userNamingConventionsConfiguration?.recordField.toNamingConventionOrDefault(base.namingConventions.recordField), + enum = userNamingConventionsConfiguration?.enum.toNamingConventionOrDefault(base.namingConventions.enum), + enumValue = userNamingConventionsConfiguration?.enumValue.toNamingConventionOrDefault(base.namingConventions.enumValue), + typeAlias = userNamingConventionsConfiguration?.typeAlias.toNamingConventionOrDefault(base.namingConventions.typeAlias), + service = userNamingConventionsConfiguration?.service.toNamingConventionOrDefault(base.namingConventions.service), + serviceOperation = userNamingConventionsConfiguration?.serviceOperation.toNamingConventionOrDefault(base.namingConventions.serviceOperation), + serviceOperationParameter = userNamingConventionsConfiguration?.serviceOperationParameter.toNamingConventionOrDefault(base.namingConventions.serviceOperationParameter), + provider = userNamingConventionsConfiguration?.provider.toNamingConventionOrDefault(base.namingConventions.provider), + samtPackage = userNamingConventionsConfiguration?.samtPackage.toNamingConventionOrDefault(base.namingConventions.samtPackage), + fileName = userNamingConventionsConfiguration?.fileName.toNamingConventionOrDefault(base.namingConventions.fileName), + ), + ) + } + + private fun DiagnosticSeverity?.toLevelOrDefault(default: CommonDiagnosticSeverity?): CommonDiagnosticSeverity? = when (this) { + null -> default + DiagnosticSeverity.Error -> CommonDiagnosticSeverity.Error + DiagnosticSeverity.Warn -> CommonDiagnosticSeverity.Warning + DiagnosticSeverity.Info -> CommonDiagnosticSeverity.Info + DiagnosticSeverity.Off -> null + } + + private fun NamingConventionsConfiguration.NamingConventions?.toNamingConventionOrDefault(default: CommonNamingConvention): CommonNamingConvention = when (this) { + null -> default + NamingConventionsConfiguration.NamingConventions.PascalCase -> CommonNamingConvention.PascalCase + NamingConventionsConfiguration.NamingConventions.CamelCase -> CommonNamingConvention.CamelCase + NamingConventionsConfiguration.NamingConventions.SnakeCase -> CommonNamingConvention.SnakeCase + NamingConventionsConfiguration.NamingConventions.KebabCase -> CommonNamingConvention.KebabCase + NamingConventionsConfiguration.NamingConventions.ScreamingSnakeCase -> CommonNamingConvention.ScreamingSnakeCase + } +} diff --git a/samt-config/src/main/kotlin/tools/samt/config/SamtLinterConfiguration.kt b/samt-config/src/main/kotlin/tools/samt/config/SamtLinterConfiguration.kt new file mode 100644 index 00000000..5021dea9 --- /dev/null +++ b/samt-config/src/main/kotlin/tools/samt/config/SamtLinterConfiguration.kt @@ -0,0 +1,66 @@ +package tools.samt.config + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SamtLinterConfiguration( + val extends: String = "recommended", + val rules: List = emptyList(), +) + +enum class DiagnosticSeverity { + @SerialName("error") + Error, + + @SerialName("warn") + Warn, + + @SerialName("info") + Info, + + @SerialName("off") + Off, +} + +@Serializable +sealed interface SamtRuleConfiguration { + val level: DiagnosticSeverity? +} + +@Serializable +@SerialName("split-model-and-providers") +data class SplitModelAndProvidersConfiguration( + override val level: DiagnosticSeverity? = null, +) : SamtRuleConfiguration + +@Serializable +@SerialName("naming-conventions") +data class NamingConventionsConfiguration( + override val level: DiagnosticSeverity? = null, + val record: NamingConventions? = null, + val recordField: NamingConventions? = null, + val enum: NamingConventions? = null, + val enumValue: NamingConventions? = null, + val typeAlias: NamingConventions? = null, + val service: NamingConventions? = null, + val serviceOperation: NamingConventions? = null, + val serviceOperationParameter: NamingConventions? = null, + val provider: NamingConventions? = null, + @SerialName("package") + val samtPackage: NamingConventions? = null, + val fileName: NamingConventions? = null, +) : SamtRuleConfiguration { + enum class NamingConventions { + @SerialName("PascalCase") + PascalCase, + @SerialName("camelCase") + CamelCase, + @SerialName("snake_case") + SnakeCase, + @SerialName("kebab-case") + KebabCase, + @SerialName("SCREAMING_SNAKE_CASE") + ScreamingSnakeCase, + } +} diff --git a/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt b/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt new file mode 100644 index 00000000..847e0954 --- /dev/null +++ b/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt @@ -0,0 +1,146 @@ +package tools.samt.config + +import com.charleskorn.kaml.YamlException +import org.junit.jupiter.api.assertThrows +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import tools.samt.common.DiagnosticSeverity as CommonDiagnosticSeverity +import tools.samt.common.NamingConventionsConfiguration as CommonNamingConventionsConfiguration +import tools.samt.common.NamingConventionsConfiguration.NamingConvention as CommonNamingConvention +import tools.samt.common.SamtLinterConfiguration as CommonLinterConfiguration +import tools.samt.common.SplitModelAndProvidersConfiguration as CommonSplitModelAndProvidersConfiguration + +class SamtConfigurationParserTest { + private val testDirectory = Path("src/test/resources/test-files") + + @BeforeTest + fun setup() { + assertTrue(testDirectory.exists() && testDirectory.isDirectory(), "Test directory does not exist") + } + + @Test + fun `works for samt-full file`() { + val samtConfiguration = SamtConfigurationParser.parseConfiguration(testDirectory.resolve("samt-full.yaml")) + + assertEquals( + tools.samt.common.SamtConfiguration( + source = "./some/other/src", + plugins = listOf( + tools.samt.common.SamtLocalPluginConfiguration( + path = "./path/to/plugin.jar" + ), + tools.samt.common.SamtMavenPluginConfiguration( + groupId = "com.example", + artifactId = "example-plugin", + version = "1.0.0", + repository = "https://repository.jboss.org/nexus/content/repositories/releases" + ), + tools.samt.common.SamtMavenPluginConfiguration( + groupId = "com.example", + artifactId = "example-plugin", + version = "1.0.0", + repository = "https://repo.spring.io/release" + ) + ), + generators = listOf( + tools.samt.common.SamtGeneratorConfiguration( + name = "samt-kotlin-ktor", + output = "./some/other/out", + options = mapOf( + "removePrefixFromSamtPackage" to "tools.samt", + "addPrefixToKotlinPackage" to "tools.samt.example.generated", + ) + ) + ) + ), samtConfiguration + ) + } + + @Test + fun `works for samt-minimal file`() { + val samtConfiguration = SamtConfigurationParser.parseConfiguration(testDirectory.resolve("samt-minimal.yaml")) + + assertEquals( + tools.samt.common.SamtConfiguration( + source = "./src", + plugins = emptyList(), + generators = listOf( + tools.samt.common.SamtGeneratorConfiguration( + name = "samt-kotlin-ktor", + output = "./out", + options = mapOf( + "addPrefixToKotlinPackage" to "com.company.samt.generated", + ) + ) + ) + ), + samtConfiguration + ) + } + + @Test + fun `throws for samt-invalid file`() { + val exception = assertThrows { + SamtConfigurationParser.parseConfiguration(testDirectory.resolve("samt-invalid.yaml")) + } + + assertEquals( + "Unknown property 'generator'. Known properties are: generators, plugins, repositories, source", + exception.message + ) + } + + @Test + fun `works for samtrc-recommended file`() { + val samtLintConfiguration = SamtConfigurationParser.parseLinterConfiguration(testDirectory.resolve(".samtrc-recommended.yaml")) + + assertEquals( + CommonLinterConfiguration( + splitModelAndProviders = CommonSplitModelAndProvidersConfiguration( + level = null, + ), + namingConventions = CommonNamingConventionsConfiguration( + level = CommonDiagnosticSeverity.Info, + record = CommonNamingConvention.PascalCase, + recordField = CommonNamingConvention.CamelCase, + enum = CommonNamingConvention.CamelCase, + enumValue = CommonNamingConvention.PascalCase, + typeAlias = CommonNamingConvention.PascalCase, + service = CommonNamingConvention.PascalCase, + serviceOperation = CommonNamingConvention.CamelCase, + serviceOperationParameter = CommonNamingConvention.CamelCase, + provider = CommonNamingConvention.PascalCase, + samtPackage = CommonNamingConvention.ScreamingSnakeCase, + fileName = CommonNamingConvention.KebabCase, + ), + ), samtLintConfiguration + ) + } + + @Test + fun `works for samtrc-strict file`() { + val samtConfiguration = SamtConfigurationParser.parseLinterConfiguration(testDirectory.resolve(".samtrc-strict.yaml")) + + assertEquals( + strict, + samtConfiguration + ) + } + + @Test + fun `throws when parsing samt-invalid file as linter configuration`() { + val exception = assertThrows { + SamtConfigurationParser.parseLinterConfiguration(testDirectory.resolve("samt-invalid.yaml")) + } + + assertEquals( + "Unknown property 'generator'. Known properties are: extends, rules", + exception.message + ) + } +} diff --git a/samt-config/src/test/resources/test-files/.samtrc-recommended.yaml b/samt-config/src/test/resources/test-files/.samtrc-recommended.yaml new file mode 100644 index 00000000..55a385b1 --- /dev/null +++ b/samt-config/src/test/resources/test-files/.samtrc-recommended.yaml @@ -0,0 +1,11 @@ +extends: recommended + +rules: + - type: split-model-and-providers + level: off + - type: naming-conventions + level: info + enum: camelCase + enumValue: PascalCase + fileName: kebab-case + package: SCREAMING_SNAKE_CASE diff --git a/samt-config/src/test/resources/test-files/.samtrc-strict.yaml b/samt-config/src/test/resources/test-files/.samtrc-strict.yaml new file mode 100644 index 00000000..c35faf33 --- /dev/null +++ b/samt-config/src/test/resources/test-files/.samtrc-strict.yaml @@ -0,0 +1 @@ +extends: strict diff --git a/samt-config/src/test/resources/test-files/samt-full.yaml b/samt-config/src/test/resources/test-files/samt-full.yaml new file mode 100644 index 00000000..fffec600 --- /dev/null +++ b/samt-config/src/test/resources/test-files/samt-full.yaml @@ -0,0 +1,22 @@ +source: ./some/other/src + +repositories: + maven: https://repository.jboss.org/nexus/content/repositories/releases + +plugins: + - type: local + path: ./path/to/plugin.jar + - type: maven + groupId: com.example + artifactId: example-plugin + version: 1.0.0 + - type: gradle + dependency: com.example:example-plugin:1.0.0 + repository: https://repo.spring.io/release + +generators: + - name: samt-kotlin-ktor + output: ./some/other/out + options: + removePrefixFromSamtPackage: tools.samt + addPrefixToKotlinPackage: tools.samt.example.generated diff --git a/samt-config/src/test/resources/test-files/samt-invalid.yaml b/samt-config/src/test/resources/test-files/samt-invalid.yaml new file mode 100644 index 00000000..efd3de8b --- /dev/null +++ b/samt-config/src/test/resources/test-files/samt-invalid.yaml @@ -0,0 +1 @@ +generator: samt-kotlin-ktor diff --git a/samt-config/src/test/resources/test-files/samt-minimal.yaml b/samt-config/src/test/resources/test-files/samt-minimal.yaml new file mode 100644 index 00000000..1642f988 --- /dev/null +++ b/samt-config/src/test/resources/test-files/samt-minimal.yaml @@ -0,0 +1,4 @@ +generators: + - name: samt-kotlin-ktor + options: + addPrefixToKotlinPackage: com.company.samt.generated diff --git a/settings.gradle.kts b/settings.gradle.kts index 34aac998..8d0febda 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,7 +5,8 @@ include( ":lexer", ":parser", ":semantic", - ":language-server" + ":language-server", + ":samt-config", ) dependencyResolutionManagement { @@ -15,6 +16,7 @@ dependencyResolutionManagement { val jCommander = "1.82" val mordant = "2.0.0-beta13" val kotlinxSerialization = "1.5.0" + val kamlVersion = "0.53.0" val kover = "0.7.0" val lsp4j = "0.20.1" @@ -23,6 +25,7 @@ dependencyResolutionManagement { library("jCommander", "com.beust", "jcommander").version(jCommander) library("mordant", "com.github.ajalt.mordant", "mordant").version(mordant) library("kotlinx.serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json").version(kotlinxSerialization) + library("kotlinx.serialization-yaml", "com.charleskorn.kaml", "kaml").version(kamlVersion) library("lsp4j", "org.eclipse.lsp4j", "org.eclipse.lsp4j").version(lsp4j) plugin("shadow", "com.github.johnrengelman.shadow").version(shadow)