1
0

Compare commits

..

27 Commits

Author SHA1 Message Date
hubert ed4d9264eb Add junit properties 2020-10-06 21:50:27 +02:00
hubert bb5e955318 Refactor models + add tests 2020-10-06 21:40:12 +02:00
hubert 6c0d17299e Fix a bug 2020-10-06 19:12:25 +02:00
hubert 8e6a14b3e0 Use version properties inside pom.xml 2020-10-06 19:07:24 +02:00
hubert a1dde23fb3 Use common versions 2020-10-06 18:40:38 +02:00
hubert f0155dea31 Refactor 2020-10-06 17:17:55 +02:00
hubert cf09799bc6 css 2020-10-06 15:42:13 +02:00
hubert a856d5e425 Display dependency fragments 2020-10-06 15:27:38 +02:00
hubert f56ec93498 Update dependencies 2020-10-05 17:42:11 +02:00
hubert 66878900f5 Clean + lint 2020-09-30 02:39:58 +02:00
hubert ce92e1fae9 Add some dependencies 2020-09-24 20:07:34 +02:00
hubert 724f1c87b1 Reduce image size 2020-09-11 22:59:56 +02:00
hubert 82214d327a Add gitignore template 2020-09-11 21:06:02 +02:00
hubert 92430510b6 Add Dockerfile 2020-09-11 19:26:13 +02:00
hubert 8a08520267 Use stream instead of zip file 2020-09-11 17:58:20 +02:00
hubert 0d47b51f3f Add project name as root for zip file 2020-09-11 17:34:31 +02:00
hubert cd628f48b7 Remove jcenter if not needed 2020-09-11 17:32:16 +02:00
hubert dc6377179c Add kotlin stdlib 2020-09-11 17:18:58 +02:00
hubert 2c0a1e44ec Fix http4k client artifact 2020-09-11 17:14:32 +02:00
hubert 6f691fcd75 Add custom repositories + arrow 2020-09-11 16:51:41 +02:00
hubert 3155a2bbe8 Fix gitignore 2020-09-11 16:27:08 +02:00
hubert 91a82c3736 Add serialization deps 2020-09-11 16:26:19 +02:00
hubert 8ea5207cde Rename templates 2020-09-11 16:13:54 +02:00
hubert deb086f4b9 Fix missing directory prefix 2020-09-11 15:46:04 +02:00
hubert c7a199fb2f Add logback config 2020-09-11 15:43:49 +02:00
hubert 658ac10375 400 when a path is entered 2020-09-10 23:26:32 +02:00
hubert 94269c7a87 Merge branch 'zip' into master 2020-09-10 23:17:02 +02:00
45 changed files with 843 additions and 261 deletions
+18
View File
@@ -0,0 +1,18 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{kt, kts}]
indent_size = 4
insert_final_newline = true
continuation_indent_size=4
max_line_length = 120
disabled_rules = no-wildcard-imports
kotlin_imports_layout = idea
+1
View File
@@ -39,3 +39,4 @@ node_modules/
.yarn-integrity .yarn-integrity
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
*.zip
+25
View File
@@ -0,0 +1,25 @@
FROM openjdk:14-alpine as jdkbuilder
RUN apk add --no-cache binutils
ENV MODULES java.base,java.xml,jdk.httpserver
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
FROM maven:3.6.3-jdk-14 as builder
WORKDIR /app
COPY pom.xml .
RUN mvn verify clean --fail-never
COPY src/main src/main
RUN mvn package
FROM alpine
ENV APPLICATION_USER app
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
COPY --from=builder /app/target/kotlin-starter*.jar /app/app.jar
COPY --from=jdkbuilder /myjdk /myjdk
COPY config.toml /app/config.toml
WORKDIR /app
EXPOSE 7000
CMD ["/myjdk/bin/java", "-server", "-Xmx64m", "-XX:+UseG1GC", "-XX:+UseStringDeduplication", "-jar", "app.jar"]
+87 -35
View File
@@ -16,92 +16,132 @@ display = "Java Version"
default = "1.4.10" default = "1.4.10"
display = "Kotlin Version" display = "Kotlin Version"
[versions]
http4k = "3.265.0"
pebble = "3.1.4"
caffeine = "2.8.5"
logback = "1.2.3"
mariadb = "2.6.2"
h2 = "1.4.200"
flyway = "7.0.0"
hikaricp = "3.4.5"
ktorm = "3.0.0"
junit = "5.7.0"
mokk = "1.10.0"
hamkrest = "1.7.0.3"
assertj = "3.17.2"
kodein-di = "7.1.0"
koin = "2.1.6"
jackson = "2.11.2"
kotlinx-serialization = "1.0-M1-1.4.0-rc"
arrow-core = "0.10.5"
[repositories]
[repositories.jcenter]
url = "https://jcenter.bintray.com"
[repositories.arrow]
url = "https://dl.bintray.com/arrow-kt/arrow-kt/"
[dependencies] [dependencies]
[dependencies.http4k] [dependencies.http4k]
groupId = "org.http4k" groupId = "org.http4k"
artifactId = "http4k-core" artifactId = "http4k-core"
version = "3.260.0" category = "http4k"
category = "server"
default = true default = true
[dependencies.http4k-server-jetty] [dependencies.http4k-server-jetty]
groupId = "org.http4k" groupId = "org.http4k"
artifactId = "http4k-server-jetty" artifactId = "http4k-server-jetty"
version = "3.260.0" version = "http4k"
category = "server" category = "http4k"
default = true default = true
logger = "org.eclipse.jetty"
[dependencies.http4k-server-apache] [dependencies.http4k-server-apache]
groupId = "org.http4k" groupId = "org.http4k"
artifactId = "http4k-server-apache" artifactId = "http4k-server-apache"
version = "3.260.0" version = "http4k"
category = "server" category = "http4k"
[dependencies.http4k-client-apache] [dependencies.http4k-client-apache]
groupId = "org.http4k" groupId = "org.http4k"
artifactId = "http4k-server-apache" artifactId = "http4k-client-apache"
version = "3.260.0" version = "http4k"
category = "server" category = "http4k"
[dependencies.javalin] [dependencies.http4k-format-jackson]
groupId = "io.javalin" groupId = "org.http4k"
artifactId = "javalin" artifactId = "http4k-format-jackson"
version = "3.10.1" version = "http4k"
category = "server" category = "http4k"
[dependencies.http4k-format-kotlinx-serialization]
groupId = "org.http4k"
artifactId = "http4k-format-kotlinx-serialization"
version = "http4k"
category = "http4k"
[dependencies.http4k-contract]
groupId = "org.http4k"
artifactId = "http4k-contract"
version = "http4k"
category = "http4k"
[dependencies.pebble] [dependencies.pebble]
groupId = "io.pebbletemplates" groupId = "io.pebbletemplates"
artifactId = "pebble" artifactId = "pebble"
version = "3.1.4"
default = true default = true
logger = "com.mitchellbosecke.pebble"
[dependencies.caffeine]
groupId = "com.github.ben-manes.caffeine"
artifactId = "caffeine"
[dependencies.logback] [dependencies.logback]
groupId = "ch.qos.logback" groupId = "ch.qos.logback"
artifactId = "logback-classic" artifactId = "logback-classic"
version = "1.2.3"
default = true default = true
[dependencies.mariadb] [dependencies.mariadb]
groupId = "org.mariadb.jdbc" groupId = "org.mariadb.jdbc"
artifactId = "mariadb-java-client" artifactId = "mariadb-java-client"
version = "2.6.2"
category = "database" category = "database"
[dependencies.h2] [dependencies.h2]
groupId = "com.h2database" groupId = "com.h2database"
artifactId = "h2" artifactId = "h2"
version = "1.4.200"
category = "database" category = "database"
[dependencies.flyway] [dependencies.flyway]
groupId = "org.flywaydb" groupId = "org.flywaydb"
artifactId = "flyway-core" artifactId = "flyway-core"
version = "6.5.4"
category = "database" category = "database"
logger = "org.flywaydb.core"
[dependencies.HikariCP] [dependencies.hikaricp]
groupId = "com.zaxxer" groupId = "com.zaxxer"
artifactId = "HikariCP" artifactId = "HikariCP"
version = "3.4.5"
category = "database" category = "database"
logger = "com.zaxxer.hikari"
[dependencies.Ktorm] [dependencies.ktorm]
groupId = "me.liuwj.ktorm" groupId = "me.liuwj.ktorm"
artifactId = "ktorm-core" artifactId = "ktorm-core"
version = "3.0.0"
category = "database" category = "database"
logger = "me.liuwj.ktorm.database"
[dependencies.Ktorm-Mysql] [dependencies.ktorm-mysql]
groupId = "me.liuwj.ktorm" groupId = "me.liuwj.ktorm"
artifactId = "ktorm-support-mysql" artifactId = "ktorm-support-mysql"
version = "3.0.0" version = "ktorm"
category = "database" category = "database"
[dependencies.junit] [dependencies.junit]
groupId = "org.junit.jupiter" groupId = "org.junit.jupiter"
artifactId = "junit-jupiter" artifactId = "junit-jupiter"
version = "5.6.2"
scope = "test" scope = "test"
category = "test" category = "test"
default = true default = true
@@ -109,7 +149,7 @@ default = true
[dependencies.junit-params] [dependencies.junit-params]
groupId = "org.junit.jupiter" groupId = "org.junit.jupiter"
artifactId = "junit-jupiter-params" artifactId = "junit-jupiter-params"
version = "5.6.2" version = "junit"
scope = "test" scope = "test"
category = "test" category = "test"
default = true default = true
@@ -117,34 +157,46 @@ default = true
[dependencies.mokk] [dependencies.mokk]
groupId = "io.mockk" groupId = "io.mockk"
artifactId = "mockk" artifactId = "mockk"
version = "1.10.0"
scope = "test" scope = "test"
category = "test" category = "test"
[dependencies.hamkrest] [dependencies.hamkrest]
groupId = "com.natpryce" groupId = "com.natpryce"
artifactId = "hamkrest" artifactId = "hamkrest"
version = "1.7.0.3"
scope = "test" scope = "test"
category = "test" category = "test"
[dependencies.assertJ] [dependencies.assertj]
groupId = "org.assertj" groupId = "org.assertj"
artifactId = "assertj-core" artifactId = "assertj-core"
version = "3.16.1"
scope = "test" scope = "test"
category = "test" category = "test"
default = true default = true
[dependencies.Kodein-DI] [dependencies.kodein-di]
groupId = "org.kodein.di" groupId = "org.kodein.di"
artifactId = "kodein-di-jvm" artifactId = "kodein-di-jvm"
version = "7.0.0"
category = "injection" category = "injection"
repository = "jcenter"
[dependencies.Koin] [dependencies.koin]
groupId = "org.koin" groupId = "org.koin"
artifactId = "koin-core" artifactId = "koin-core"
version = "2.1.6"
category = "injection" category = "injection"
repository = "jcenter"
default = true default = true
[dependencies.jackson]
groupId = "com.fasterxml.jackson.module"
artifactId = "jackson-module-kotlin"
category = "serialization"
[dependencies.kotlinx-serialization]
groupId = "org.jetbrains.kotlinx"
artifactId = "kotlinx-serialization-runtime"
category = "serialization"
[dependencies.arrow-core]
groupId = "io.arrow-kt"
artifactId = "arrow-core"
repository = "arrow"
+45 -9
View File
@@ -10,6 +10,7 @@
<maven.compiler.source>11</maven.compiler.source> <maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>1.4.10</kotlin.version> <kotlin.version>1.4.10</kotlin.version>
<kotlin.code.style>official</kotlin.code.style>
</properties> </properties>
<repositories> <repositories>
@@ -26,11 +27,6 @@
<artifactId>pebble</artifactId> <artifactId>pebble</artifactId>
<version>3.1.4</version> <version>3.1.4</version>
</dependency> </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency> <dependency>
<groupId>com.electronwill.night-config</groupId> <groupId>com.electronwill.night-config</groupId>
<artifactId>toml</artifactId> <artifactId>toml</artifactId>
@@ -42,9 +38,9 @@
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.javalin</groupId> <groupId>org.http4k</groupId>
<artifactId>javalin</artifactId> <artifactId>http4k-core</artifactId>
<version>3.10.1</version> <version>3.265.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
@@ -56,6 +52,24 @@
<artifactId>koin-core</artifactId> <artifactId>koin-core</artifactId>
<version>2.1.6</version> <version>2.1.6</version>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.17.2</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -74,13 +88,35 @@
<goal>shade</goal> <goal>shade</goal>
</goals> </goals>
<configuration> <configuration>
<minimizeJar>false</minimizeJar> <minimizeJar>true</minimizeJar>
<transformers> <transformers>
<transformer <transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>starter.KotlinStarterKt</mainClass> <mainClass>starter.KotlinStarterKt</mainClass>
</transformer> </transformer>
</transformers> </transformers>
<filters>
<filter>
<artifact>com.electronwill.night-config:*</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/maven/**</exclude>
<exclude>META-INF/proguard/**</exclude>
<exclude>META-INF/*.kotlin_module</exclude>
<exclude>META-INF/DEPENDENCIES*</exclude>
<exclude>META-INF/NOTICE*</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>LICENSE*</exclude>
<exclude>META-INF/README*</exclude>
<exclude>META-INF/native-image/**</exclude>
</excludes>
</filter>
</filters>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
+2 -1
View File
@@ -1,7 +1,8 @@
module.exports = { module.exports = {
purge: { purge: {
content: [ content: [
'../main/resources/views/**/*.twig' '../main/resources/views/**/*.twig',
'../main/kotlin/starter/PebbleModule.kt',
] ]
}, },
theme: {}, theme: {},
-35
View File
@@ -1,35 +0,0 @@
package starter
import com.electronwill.nightconfig.core.file.FileConfig
import com.electronwill.nightconfig.core.Config as NightConfig
data class StarterConfig(val dependencies: List<Dependency>, val inputs: List<Input>)
class Config {
fun load(): StarterConfig {
val cfg = FileConfig.of("config.toml")
cfg.load()
val dependenciesNode: NightConfig = cfg["dependencies"]
@Suppress("UNCHECKED_CAST") val dependenciesMap = dependenciesNode.valueMap() as Map<String, NightConfig>
val dependencies = dependenciesMap.map { (name, values) ->
Dependency(
name,
values["groupId"],
values["artifactId"],
values["version"],
values.getOrElse("default", false),
values.getEnumOrElse("category", Category.Other),
values.getEnumOrElse("scope", Scope.Compile),
)
}
val inputsNode: NightConfig = cfg["inputs"]
@Suppress("UNCHECKED_CAST") val inputMap = inputsNode.valueMap() as Map<String, NightConfig>
val inputs = inputMap.map { (name, values) ->
Input(name, values["display"], values["default"])
}
return StarterConfig(dependencies, inputs)
}
}
+7 -23
View File
@@ -1,29 +1,13 @@
package starter package starter
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.bind import starter.modules.mainModule
import org.koin.dsl.module import starter.modules.pebbleModule
import starter.templates.MainTemplate import starter.modules.routesModule
import starter.templates.PomTemplate import starter.modules.templateModule
import starter.templates.Template
val mainModule = module {
single { Config().load() }
single { PebbleModule().engine() }
single { Server(get(), get(), get()) }
single { Views(get()) }
single { ProjectZip(getAll()) }
}
val templateModule = module {
single { PomTemplate(get()) } bind Template::class
single { MainTemplate(get()) } bind Template::class
}
fun main() { fun main() {
val koin = startKoin { startKoin {
modules(mainModule, templateModule) modules(mainModule, pebbleModule, templateModule, routesModule)
}.koin }
val server = koin.get<Server>()
server.run()
} }
+19 -12
View File
@@ -1,7 +1,7 @@
package starter package starter
enum class Category { enum class Category {
Server, Injection, Database, Test, Other Http4k, Injection, Database, Serialization, Test, Other
} }
enum class Scope { enum class Scope {
@@ -9,20 +9,27 @@ enum class Scope {
} }
data class Dependency( data class Dependency(
val name: String, val name: String,
val groupId: String, val groupId: String,
val artifactId: String, val artifactId: String,
val version: String, val version: Version,
val default: Boolean, val default: Boolean,
val category: Category, val category: Category,
val scope: Scope val scope: Scope,
val logger: String?,
val repository: Repository?,
) )
data class Repository(val name: String, val url: String)
data class Input(val name: String, val display: String, val value: String? = null) data class Input(val name: String, val display: String, val value: String? = null)
data class Project( data class Project(
val name: String, val name: String,
val basePackage: String, val basePackage: String,
val inputs: List<Input>, val inputs: List<Input>,
val dependencies: List<Dependency>, val dependencies: List<Dependency>,
val repositories: Collection<Repository>,
) )
data class Version(val name: String, val value: String)
-15
View File
@@ -1,15 +0,0 @@
package starter
import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.loader.ClasspathLoader
class PebbleModule {
fun engine(): PebbleEngine {
val loader = ClasspathLoader()
loader.suffix = ".twig"
return PebbleEngine.Builder()
.loader(loader)
.cacheActive(false)
.build()
}
}
+8 -11
View File
@@ -3,21 +3,18 @@ package starter
import starter.templates.Template import starter.templates.Template
import starter.utils.ZipOutput import starter.utils.ZipOutput
import starter.utils.sanitizeFilename import starter.utils.sanitizeFilename
import java.io.ByteArrayOutputStream
class ProjectZip(private val templates: List<Template>) { class ProjectZip(private val templates: List<Template>) {
fun createZip(project: Project): String { fun createZip(project: Project): ByteArrayOutputStream {
val name: String val projectName = sanitizeFilename(project.name)
val zipOutput = ZipOutput()
ZipOutput(sanitizeFilename(project.name)).use { zipOutput.use { zip ->
name = it.name templates.filter { it.enabled(project) }.forEach { template ->
zip.write(projectName + "/" + template.path(project), template.render(project))
templates.forEach { template ->
it.write(template.path(project), template.render(project))
} }
} }
return zipOutput.outputStream
return name
} }
} }
-44
View File
@@ -1,44 +0,0 @@
package starter
import io.javalin.Javalin
import starter.utils.sanitizeFilename
import java.io.File
class Server(
private val views: Views,
private val conf: StarterConfig,
private val projectZip: ProjectZip,
) {
fun run() {
val app = Javalin.create {
it.addStaticFiles("/assets")
}.start(7000)
app.get("/") { ctx ->
ctx.result(views.index(conf.dependencies, conf.inputs))
ctx.contentType("text/html")
}
app.post("/") { ctx ->
val deps = conf.dependencies.filter {
ctx.formParam(it.name) != null
}
val inputKeys = conf.inputs.map { it.name }
val inputs = ctx.formParamMap()
.filter { it.key in inputKeys }
.map { (name, value) ->
conf.inputs.find { it.name == name }!!.copy(value = value.first())
}
val projectName = inputs.find { it.name == "name" }!!.value!!
val basePackage = inputs.find { it.name == "basePackage" }!!.value!!
val project = Project(projectName, basePackage, inputs, deps)
ctx.contentType("application/zip")
ctx.header("Content-Disposition", "attachment; filename=\"${sanitizeFilename(projectName)}.zip\"")
val zipFile = projectZip.createZip(project)
ctx.result(File(zipFile).readBytes())
}
}
}
-17
View File
@@ -1,17 +0,0 @@
package starter
import com.mitchellbosecke.pebble.PebbleEngine
import org.slf4j.LoggerFactory
import starter.utils.render
class Views(private val engine: PebbleEngine) {
private val logger = LoggerFactory.getLogger(javaClass)
fun index(dependencies: List<Dependency>, inputs: List<Input>): String {
val dependenciesByCategory = dependencies.groupBy { it.category }.toSortedMap()
return engine.render("views/index",
mapOf("dependencies" to dependenciesByCategory, "inputs" to inputs)
)
}
}
+58
View File
@@ -0,0 +1,58 @@
package starter.config
import com.electronwill.nightconfig.core.UnmodifiableConfig
import com.electronwill.nightconfig.core.file.FileConfig
import starter.*
import com.electronwill.nightconfig.core.Config as NightConfig
data class StarterConfig(
val dependencies: List<Dependency>,
val inputs: List<Input>,
)
class Config {
@Suppress("UNCHECKED_CAST")
private fun FileConfig.configMap(key: String) = this.get<NightConfig>(key).valueMap() as Map<String, NightConfig>
fun load(): StarterConfig {
val cfg = FileConfig.of("config.toml")
cfg.load()
@Suppress("UNCHECKED_CAST")
val versions = cfg.get<UnmodifiableConfig>("versions").valueMap() as Map<String, String>
val repositories = cfg.configMap("repositories")
.map { (name, values) ->
Repository(name, values["url"])
}
val dependencies = cfg.configMap("dependencies")
.map { (name, values) ->
val versionKey: String = values["version"] ?: name
val version = versions[versionKey] ?: error("Missing version for $name")
val repositoryName: String? = values["repository"]
val repo = repositoryName?.let { repoName -> repositories.find { it.name == repoName } }
Dependency(
name = name,
groupId = values["groupId"],
artifactId = values["artifactId"],
version = Version(versionKey, version),
default = values.getOrElse("default", false),
category = values.getEnumOrElse("category", Category.Other),
scope = values.getEnumOrElse("scope", Scope.Compile),
logger = values["logger"],
repository = repo,
)
}
val inputs = cfg.configMap("inputs")
.map { (name, values) ->
Input(name, values["display"], values["default"])
}
return StarterConfig(dependencies, inputs)
}
}
@@ -0,0 +1,17 @@
@file:Suppress("NOTHING_TO_INLINE")
package starter.extensions
import org.http4k.core.Response
import org.http4k.core.Status
import starter.utils.sanitizeFilename
import java.io.InputStream
inline fun Response.Companion.ok() = Response(Status.OK)
inline fun Response.Companion.badRequest() = Response(Status.BAD_REQUEST)
fun attachment(value: InputStream, name: String, contentType: String) = { res: Response ->
res.header("Content-Type", contentType)
.header("Content-Disposition", "attachment; filename=\"${sanitizeFilename(name)}\"")
.body(value)
}
@@ -0,0 +1,24 @@
package starter.modules
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.SunHttp
import org.http4k.server.asServer
import org.koin.dsl.module
import org.koin.dsl.onClose
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import starter.ProjectZip
import starter.config.Config
import starter.views.Views
val mainModule = module {
single(createdAtStart = true) {
get<Logger>().info("Starting on http://localhost:7000")
get<RoutingHttpHandler>().asServer(SunHttp(7000)).start()
} onClose { it?.stop() }
single { Config().load() }
single { LoggerFactory.getLogger("Starter") }
single { Views(get(), get()) }
single { ProjectZip(getAll()) }
}
@@ -0,0 +1,58 @@
package starter.modules
import com.mitchellbosecke.pebble.extension.escaper.SafeString
import com.mitchellbosecke.pebble.template.EvaluationContext
import com.mitchellbosecke.pebble.template.PebbleTemplate
import org.intellij.lang.annotations.Language
import org.koin.dsl.bind
import org.koin.dsl.module
import starter.Dependency
import starter.utils.PebbleEngineBuilder
import starter.utils.PebbleEngineBuilder.CacheType.ConcurrentMap
import starter.utils.PebbleFunction
class DepAsXmlPebbleFunction : PebbleFunction {
override val name = "depAsXml"
override fun getArgumentNames() = listOf("dependency")
override fun execute(args: Map<String, Any>, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any {
val dep = args["dependency"] as Dependency
fun startTag(name: String): String {
@Language("html") @Suppress("UnnecessaryVariable")
val result = """<span class="text-gray-700">&lt;</span><span class="text-red-700">$name</span><span class="text-gray-700">&gt;</span>"""
return result
}
fun endTag(name: String): String {
@Language("html") @Suppress("UnnecessaryVariable")
val result = """<span class="text-gray-700">&lt;/</span><span class="text-red-700">$name</span><span class="text-gray-700">&gt;</span>"""
return result
}
fun tag(name: String, content: String): String {
return """${startTag(name)}$content${endTag(name)}"""
}
val result = """
|${startTag("dependency")}
| ${tag("groupId", dep.groupId)}
| ${tag("artifactId", dep.artifactId)}
| ${tag("version", dep.version.value)}
|${endTag("dependency")}
""".trimMargin()
return SafeString(result)
}
}
val pebbleModule = module {
single { DepAsXmlPebbleFunction() } bind PebbleFunction::class
single {
PebbleEngineBuilder {
cache = ConcurrentMap
functions(getAll())
classPath { suffix = ".twig" }
}
}
}
@@ -0,0 +1,25 @@
package starter.modules
import org.http4k.core.then
import org.http4k.filter.ServerFilters
import org.http4k.routing.ResourceLoader
import org.http4k.routing.routes
import org.http4k.routing.static
import org.koin.dsl.bind
import org.koin.dsl.module
import starter.routes.IndexRouteSupplier
import starter.routes.RouteSupplier
import starter.routes.ZipRouteSupplier
import starter.routes.toRouter
val routesModule = module {
single { IndexRouteSupplier(get()) } bind RouteSupplier::class
single { ZipRouteSupplier(get(), get()) } bind RouteSupplier::class
single {
ServerFilters.CatchAll().then(
routes(
static(ResourceLoader.Classpath("/assets")),
getAll<RouteSupplier>().toRouter()
))
}
}
@@ -0,0 +1,13 @@
package starter.modules
import org.koin.dsl.bind
import org.koin.dsl.module
import starter.templates.*
val templateModule = module {
single { PomTemplate(get()) } bind Template::class
single { MainTemplate(get()) } bind Template::class
single { LogbackTemplate(get()) } bind Template::class
single { GitignoreTemplate(get()) } bind Template::class
single { JunitTemplate(get()) } bind Template::class
}
@@ -0,0 +1,15 @@
package starter.routes
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.routing.bind
import starter.views.Views
class IndexRouteSupplier(private val views: Views) : RouteSupplier {
override fun get() = "/" bind Method.GET to {
Response(Status.OK)
.body(views.index())
.header("Content-Type", "text/html")
}
}
@@ -0,0 +1,10 @@
package starter.routes
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.routes
interface RouteSupplier {
fun get(): RoutingHttpHandler
}
fun List<RouteSupplier>.toRouter() = routes(*map { it.get() }.toTypedArray())
@@ -0,0 +1,54 @@
package starter.routes
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.body.form
import org.http4k.core.body.formAsMap
import org.http4k.core.with
import org.http4k.routing.bind
import starter.Project
import starter.ProjectZip
import starter.config.StarterConfig
import starter.extensions.attachment
import starter.extensions.badRequest
import starter.extensions.ok
import java.io.ByteArrayInputStream
class ZipRouteSupplier(
private val conf: StarterConfig,
private val projectZip: ProjectZip,
) : RouteSupplier {
override fun get() = "/" bind Method.POST to { req ->
val deps = conf.dependencies.filter {
req.form(it.name) != null
}
val inputKeys = conf.inputs.map { it.name }
val inputs = req.formAsMap()
.filter { it.key in inputKeys }
.map { (name, value) ->
conf.inputs.find { it.name == name }!!.copy(value = value.first())
}
val projectName = inputs.find { it.name == "name" }!!.value!!
val basePackage = inputs.find { it.name == "basePackage" }!!.value!!
if (basePackage.contains("/") || basePackage.contains("..")) {
Response.badRequest()
} else {
val repositories = deps.mapNotNull { it.repository }.toSet()
val project = Project(projectName, basePackage, inputs, deps, repositories)
val outputStream = projectZip.createZip(project)
Response.ok().with(
attachment(
value = ByteArrayInputStream(outputStream.toByteArray()),
name = "$projectName.zip",
contentType = "application/zip"
)
)
}
}
}
@@ -0,0 +1,13 @@
package starter.templates
import com.mitchellbosecke.pebble.PebbleEngine
import starter.Project
import starter.utils.render
class GitignoreTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) =
".gitignore"
override fun render(project: Project) =
engine.render("starter/gitignore/index")
}
@@ -0,0 +1,12 @@
package starter.templates
import com.mitchellbosecke.pebble.PebbleEngine
import starter.Project
import starter.utils.prettyPrintXml
import starter.utils.render
class JunitTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) = "src/test/resources/junit-platform.properties"
override fun enabled(project: Project) = project.dependencies.any { it.name == "junit" }
override fun render(project: Project) = engine.render("starter/junit/index")
}
@@ -0,0 +1,20 @@
package starter.templates
import com.mitchellbosecke.pebble.PebbleEngine
import starter.Project
import starter.utils.prettyPrintXml
import starter.utils.render
class LogbackTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) = "src/main/resources/logback.xml"
override fun enabled(project: Project) = project.dependencies.any { it.name == "logback" }
override fun render(project: Project): String {
val args = mapOf(
"loggers" to project.dependencies.mapNotNull { it.logger }.toSet()
)
val rendered = engine.render("starter/logback/index", args)
return prettyPrintXml(rendered)
}
}
@@ -6,8 +6,8 @@ import starter.utils.render
class MainTemplate(private val engine: PebbleEngine) : Template { class MainTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) = override fun path(project: Project) =
project.basePackage.replace('.', '/') + "/" + project.name.toLowerCase().capitalize() + ".kt" "src/main/kotlin/" + project.basePackage.replace('.', '/') + "/" + project.name.toLowerCase().capitalize() + ".kt"
override fun render(project: Project) = override fun render(project: Project) =
engine.render("starter/main/main", mapOf("basePackage" to project.basePackage)) engine.render("starter/main/index", mapOf("basePackage" to project.basePackage))
} }
@@ -2,6 +2,7 @@ package starter.templates
import com.mitchellbosecke.pebble.PebbleEngine import com.mitchellbosecke.pebble.PebbleEngine
import starter.Project import starter.Project
import starter.Version
import starter.utils.prettyPrintXml import starter.utils.prettyPrintXml
import starter.utils.render import starter.utils.render
@@ -10,14 +11,17 @@ class PomTemplate(private val engine: PebbleEngine) : Template {
override fun render(project: Project): String { override fun render(project: Project): String {
val args: MutableMap<String, Any?> = mutableMapOf( val args: MutableMap<String, Any?> = mutableMapOf(
"dependencies" to project.dependencies.sortedBy { it.scope }, "dependencies" to project.dependencies.sortedBy { it.scope },
"repositories" to project.repositories,
"kotlinxSerialization" to project.dependencies.any { it.name == "kotlinx-serialization" },
"versions" to project.dependencies.map { it.version }.toSet()
) )
project.inputs.forEach { project.inputs.forEach {
args[it.name] = it.value args[it.name] = it.value
} }
val rendered = engine.render("starter/pom/pom", args) val rendered = engine.render("starter/pom/index", args)
return prettyPrintXml(rendered) return prettyPrintXml(rendered)
} }
} }
@@ -4,5 +4,6 @@ import starter.Project
interface Template { interface Template {
fun path(project: Project): String fun path(project: Project): String
fun enabled(project: Project): Boolean = true
fun render(project: Project): String fun render(project: Project): String
} }
+81 -5
View File
@@ -1,11 +1,87 @@
package starter.utils package starter.utils
import com.mitchellbosecke.pebble.PebbleEngine import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.cache.tag.CaffeineTagCache
import com.mitchellbosecke.pebble.cache.tag.ConcurrentMapTagCache
import com.mitchellbosecke.pebble.cache.tag.NoOpTagCache
import com.mitchellbosecke.pebble.cache.template.CaffeineTemplateCache
import com.mitchellbosecke.pebble.cache.template.ConcurrentMapTemplateCache
import com.mitchellbosecke.pebble.cache.template.NoOpTemplateCache
import com.mitchellbosecke.pebble.extension.AbstractExtension
import com.mitchellbosecke.pebble.extension.Filter
import com.mitchellbosecke.pebble.extension.Function
import com.mitchellbosecke.pebble.loader.ClasspathLoader
import java.io.StringWriter import java.io.StringWriter
fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()): String { fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()) =
val template = getTemplate(name) getTemplate(name).let { StringWriter().apply { it.evaluate(this, args) }.toString() }
val writer = StringWriter()
template.evaluate(writer, args) fun PebbleEngine.render(name: String, vararg args: Pair<String, Any?>) =
return writer.toString() render(name, mapOf(*args))
interface PebbleFunction : Function {
val name: String
}
interface PebbleFilter : Filter {
val name: String
}
class PebbleEngineBuilder {
enum class CacheType {
None, Caffeine, ConcurrentMap
}
private var loader = ClasspathLoader()
var cache = CacheType.ConcurrentMap
private val functions = mutableListOf<PebbleFunction>()
private val filters = mutableListOf<PebbleFilter>()
fun function(function: PebbleFunction) {
functions.add(function)
}
fun functions(functions: Iterable<PebbleFunction>) {
this.functions.addAll(functions)
}
fun filter(filter: PebbleFilter) {
filters.add(filter)
}
fun filters(filters: Iterable<PebbleFilter>) {
this.filters.addAll(filters)
}
fun classPath(block: ClasspathLoader.() -> Unit) {
loader = ClasspathLoader().apply(block)
}
companion object {
operator fun invoke(block: PebbleEngineBuilder.() -> Unit): PebbleEngine =
PebbleEngineBuilder().apply(block).let { builder ->
PebbleEngine.Builder()
.loader(builder.loader)
.cacheActive(builder.cache != CacheType.None)
.templateCache(when (builder.cache) {
CacheType.None -> NoOpTemplateCache()
CacheType.Caffeine -> CaffeineTemplateCache()
CacheType.ConcurrentMap -> ConcurrentMapTemplateCache()
})
.tagCache(when (builder.cache) {
CacheType.None -> NoOpTagCache()
CacheType.Caffeine -> CaffeineTagCache()
CacheType.ConcurrentMap -> ConcurrentMapTagCache()
})
.extension(object : AbstractExtension() {
override fun getFunctions(): Map<String, Function>? =
builder.functions.associateBy { it.name }.ifEmpty { null }
override fun getFilters(): Map<String, Filter>? =
builder.filters.associateBy { it.name }.ifEmpty { null }
})
.build()
}
}
} }
+7 -6
View File
@@ -13,22 +13,23 @@ import javax.xml.transform.stream.StreamResult
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
/* /*
@see https://stackoverflow.com/a/33541820 @see https://stackoverflow.com/a/33541820
*/ */
fun prettyPrintXml(xml: String, indent: Int = 4): String { fun prettyPrintXml(xml: String, indent: Int = 4): String {
// Turn xml string into a document // Turn xml string into a document
val document: Document = DocumentBuilderFactory.newInstance() val document: Document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder() .newDocumentBuilder()
.parse(InputSource(ByteArrayInputStream(xml.encodeToByteArray()))) .parse(InputSource(ByteArrayInputStream(xml.encodeToByteArray())))
// Remove whitespaces outside tags // Remove whitespaces outside tags
document.normalize() document.normalize()
val xPath = XPathFactory.newInstance().newXPath() val xPath = XPathFactory.newInstance().newXPath()
val nodeList = xPath.evaluate("//text()[normalize-space()='']", val nodeList = xPath.evaluate(
document, "//text()[normalize-space()='']",
XPathConstants.NODESET) as NodeList document,
XPathConstants.NODESET
) as NodeList
for (i in 0 until nodeList.length) { for (i in 0 until nodeList.length) {
val node = nodeList.item(i) val node = nodeList.item(i)
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
+9 -16
View File
@@ -2,28 +2,21 @@ package starter.utils
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import java.io.File import java.io.ByteArrayOutputStream
import java.nio.file.Paths
class ZipOutput(name: String) : AutoCloseable { class ZipOutput : AutoCloseable {
private val baseDir = File(".").canonicalFile val outputStream = ByteArrayOutputStream()
private val zipPath = Paths.get(baseDir.path, "$name.zip") private val zipOutputStream = ZipArchiveOutputStream(outputStream)
private val zipFile = zipPath.toAbsolutePath().normalize().toFile().apply {
createNewFile()
}
private val outputStream = ZipArchiveOutputStream(zipFile.outputStream())
val name: String
get() = zipFile.name
fun write(path: String, content: String) { fun write(path: String, content: String) {
val entry = ZipArchiveEntry(path) val entry = ZipArchiveEntry(path)
outputStream.putArchiveEntry(entry) zipOutputStream.putArchiveEntry(entry)
outputStream.write(content.toByteArray()) zipOutputStream.write(content.toByteArray())
outputStream.closeArchiveEntry() zipOutputStream.closeArchiveEntry()
} }
override fun close() { override fun close() {
outputStream.finish() zipOutputStream.finish()
outputStream.close() zipOutputStream.close()
} }
} }
+14
View File
@@ -0,0 +1,14 @@
package starter.views
import com.mitchellbosecke.pebble.PebbleEngine
import starter.config.StarterConfig
import starter.utils.render
class Views(private val engine: PebbleEngine, config: StarterConfig) {
private val args = mapOf(
"dependencies" to config.dependencies.groupBy { it.category }.toSortedMap(),
"inputs" to config.inputs
)
fun index() = engine.render("views/index", args)
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
target/
.idea/
*.iml
*.ipr
*.iws
@@ -0,0 +1,4 @@
junit.jupiter.testinstance.lifecycle.default=per_class
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
@@ -9,6 +9,7 @@
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="com.mitchellbosecke.pebble" level="INFO"/> {% for logger in loggers %}
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="{{ logger }}" level="INFO"/>
{% endfor %}
</configuration> </configuration>
@@ -1,11 +1,16 @@
<dependencies> <dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
{% for dep in dependencies %} {% for dep in dependencies %}
<dependency> <dependency>
<groupId>{{ dep.groupId }}</groupId> <groupId>{{ dep.groupId }}</groupId>
<artifactId>{{ dep.artifactId }}</artifactId> <artifactId>{{ dep.artifactId }}</artifactId>
<version>{{ dep.version }}</version> <version>{{ "${" }}{{ dep.version.name }}{{ ".version}" }}</version>
{% if dep.scope == "Test" %} {% if dep.scope == "Test" %}
<scope>test</scope> <scope>test</scope>
{% endif %} {% endif %}
</dependency> </dependency>
{% endfor %} {% endfor %}
@@ -1,7 +1,10 @@
{% if repositories is not empty %}
<repositories> <repositories>
<repository> {% for repo in repositories %}
<id>jcenter</id> <repository>
<name>jcenter</name> <id>{{ repo.name }}</id>
<url>https://jcenter.bintray.com</url> <url>{{ repo.url }}</url>
</repository> </repository>
{% endfor %}
</repositories> </repositories>
{% endif %}
@@ -12,6 +12,11 @@
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>{{ basePackage }}/{{ name | lower | capitalize }}Kt</main.class> <main.class>{{ basePackage }}/{{ name | lower | capitalize }}Kt</main.class>
<!-- versions -->
{% for version in versions %}
<{{version.name}}.version>{{version.value}}</{{version.name}}.version>
{% endfor %}
</properties> </properties>
{% include "starter/pom/@dependencies" %} {% include "starter/pom/@dependencies" %}
@@ -20,5 +20,19 @@
</executions> </executions>
<configuration> <configuration>
<jvmTarget>${java.version}</jvmTarget> <jvmTarget>${java.version}</jvmTarget>
{% if kotlinxSerialization %}
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
{% endif %}
</configuration> </configuration>
{% if kotlinxSerialization %}
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
{% endif %}
</plugin> </plugin>
@@ -9,6 +9,7 @@
<goal>shade</goal> <goal>shade</goal>
</goals> </goals>
<configuration> <configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>true</minimizeJar> <minimizeJar>true</minimizeJar>
<transformers> <transformers>
<transformer <transformer
+10 -5
View File
@@ -1,9 +1,14 @@
{% macro dependency(dependency) %} {% macro dependency(dependency) %}
<label class="m-2"> <details>
<input name="{{ dependency.name }}" <summary>
type="checkbox"{% if dependency.default %} checked{% endif %}> <label class="m-2">
<span>{{ dependency.name }}</span> <input name="{{ dependency.name }}"
</label> type="checkbox"{% if dependency.default %} checked{% endif %}>
<span>{{ dependency.name }}</span>
</label>
</summary>
<pre><code>{{ depAsXml(dependency) }}</code></pre>
</details>
{% endmacro %} {% endmacro %}
{% macro input(input) %} {% macro input(input) %}
+1 -1
View File
@@ -21,6 +21,6 @@
</section> </section>
{% endfor %} {% endfor %}
</div> </div>
<button type="submit" class="w-full btn btn-purple">Submit</button> <button type="submit" class="my-4 w-full btn btn-purple">Submit</button>
</form> </form>
{% endblock %} {% endblock %}
@@ -0,0 +1,121 @@
package starter.templates
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.koin.dsl.koinApplication
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import starter.Project
import starter.config.StarterConfig
import starter.modules.mainModule
import starter.modules.pebbleModule
import starter.modules.templateModule
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class PomTemplateTest {
private val koin = koinApplication {
modules(mainModule, pebbleModule, templateModule)
}.koin
private val pomTemplate = koin.get<PomTemplate>()
private val conf = koin.get<StarterConfig>()
private val project = Project(
name = "Test",
basePackage = "org.test",
inputs = conf.inputs,
dependencies = conf.dependencies,
repositories = conf.dependencies.mapNotNull { it.repository }.toSet()
)
private val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
private val xPath = XPathFactory.newInstance().newXPath()
// region xml utils
private fun String.extract(expression: String): String {
val doc = docBuilder.parse(this.byteInputStream())
xPath.compile(expression).evaluate(doc, XPathConstants.NODESET)
return xPath.compile(expression).evaluate(doc)
}
private fun <T> String.extractAll(expression: String, mapper: (Node) -> T): List<T> {
val doc = docBuilder.parse(this.byteInputStream())
val res = xPath.compile(expression).evaluate(doc, XPathConstants.NODESET) as NodeList
return res.asList().map(mapper)
}
private fun NodeList.asList(): List<Node> = (0 until length).map { item(it) }
// endregion
@Test
fun javaVersion() {
val xml = pomTemplate.render(project)
assertThat(xml.extract("/project/properties/java.version"))
.isEqualTo(project.inputs.find { it.name == "javaVersion" }?.value)
println(xml)
}
@Test
fun dependencies() {
val xml = pomTemplate.render(project)
val deps = xml.extractAll("/project/dependencies/dependency") {
val map = it.childNodes.asList()
.filter { it.nodeType == Node.ELEMENT_NODE }
.associate { it.nodeName to it.firstChild.nodeValue }
Triple(map["groupId"]!!, map["artifactId"]!!, map["version"]!!)
}.filterNot { it.second == "kotlin-stdlib-jdk8" }
println(deps.joinToString("\n"))
val expectedDependencies = project.dependencies
.map { Triple(it.groupId, it.artifactId, "\${" + it.version.name + ".version}") }
assertThat(expectedDependencies).containsExactlyInAnyOrderElementsOf(deps)
}
@Test
fun versions() {
val xml = pomTemplate.render(project)
val versions = xml.extractAll("/project/properties") {
it.childNodes.asList()
.filter { it.nodeType == Node.ELEMENT_NODE }
.filter { it.nodeName.endsWith(".version") }
.filterNot { it.nodeName in listOf("java.version", "kotlin.version") }
.associate { it.nodeName.substringBefore(".version") to it.firstChild.nodeValue }
}.first()
val expected = project.dependencies.associate { it.version.name to it.version.value }
assertThat(versions).containsExactlyInAnyOrderEntriesOf(expected)
println(versions)
}
@Test
fun kotlinxSerialization() {
val xml = pomTemplate.render(project)
@Language("XPath")
val kotlinMavenPlugin = "/project/build/plugins/plugin[artifactId='kotlin-maven-plugin']"
val kotlinxPlugin = "$kotlinMavenPlugin/configuration/compilerPlugins/plugin"
assertThat(xml.extract(kotlinxPlugin))
.isEqualTo("kotlinx-serialization")
val kotlinxPluginDep = "$kotlinMavenPlugin/dependencies/dependency/artifactId"
assertThat(xml.extract(kotlinxPluginDep))
.isEqualTo("kotlin-maven-serialization")
}
}