1
0

Compare commits

...

15 Commits

Author SHA1 Message Date
hubert 8bcc50ea1b Add tests 2020-10-08 16:28:11 +02:00
hubert d063ba1c25 Add editorconfig 2020-10-08 15:38:42 +02:00
hubert 68fdac4d64 Add features + ktlint as first feature 2020-10-08 15:35:25 +02:00
hubert aa585358be Add ktlint plugin to maven 2020-10-08 14:48:11 +02:00
hubert d84770970d Use kotlin bom 2020-10-07 01:53:42 +02:00
hubert fe03d64122 Upgrade to java 14 2020-10-06 23:51:38 +02:00
hubert 13e269842b Unload config after start 2020-10-06 23:41:44 +02:00
hubert d4635f82ae Add config loading tests 2020-10-06 23:35:59 +02:00
hubert 009ddd3d16 Use name as default artifactId 2020-10-06 22:56:37 +02:00
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
35 changed files with 939 additions and 233 deletions
+43 -47
View File
@@ -16,6 +16,31 @@ display = "Java Version"
default = "1.4.10"
display = "Kotlin Version"
[features]
[features.ktlint]
default = true
[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"
mockk = "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]
@@ -29,109 +54,90 @@ url = "https://dl.bintray.com/arrow-kt/arrow-kt/"
[dependencies.http4k]
groupId = "org.http4k"
artifactId = "http4k-core"
version = "3.264.0"
category = "http4k"
default = true
[dependencies.http4k-server-jetty]
groupId = "org.http4k"
artifactId = "http4k-server-jetty"
version = "3.264.0"
version = "http4k"
category = "http4k"
default = true
logger = "org.eclipse.jetty"
[dependencies.http4k-server-apache]
groupId = "org.http4k"
artifactId = "http4k-server-apache"
version = "3.264.0"
version = "http4k"
category = "http4k"
[dependencies.http4k-client-apache]
groupId = "org.http4k"
artifactId = "http4k-client-apache"
version = "3.264.0"
version = "http4k"
category = "http4k"
[dependencies.http4k-format-jackson]
groupId = "org.http4k"
artifactId = "http4k-format-jackson"
version = "3.264.0"
version = "http4k"
category = "http4k"
[dependencies.http4k-format-kotlinx-serialization]
groupId = "org.http4k"
artifactId = "http4k-format-kotlinx-serialization"
version = "3.264.0"
version = "http4k"
category = "http4k"
[dependencies.http4k-contract]
groupId = "org.http4k"
artifactId = "http4k-contract"
version = "3.264.0"
version = "http4k"
category = "http4k"
[dependencies.pebble]
groupId = "io.pebbletemplates"
artifactId = "pebble"
version = "3.1.4"
default = true
logger = "com.mitchellbosecke.pebble"
[dependencies.caffeine]
groupId = "com.github.ben-manes.caffeine"
artifactId = "caffeine"
version = "2.8.5"
[dependencies.logback]
groupId = "ch.qos.logback"
artifactId = "logback-classic"
version = "1.2.3"
default = true
[dependencies.mariadb]
groupId = "org.mariadb.jdbc"
artifactId = "mariadb-java-client"
version = "2.6.2"
category = "database"
[dependencies.h2]
groupId = "com.h2database"
artifactId = "h2"
version = "1.4.200"
category = "database"
[dependencies.flyway]
groupId = "org.flywaydb"
artifactId = "flyway-core"
version = "6.5.4"
category = "database"
logger = "org.flywaydb.core"
[dependencies.HikariCP]
[dependencies.hikaricp]
groupId = "com.zaxxer"
artifactId = "HikariCP"
version = "3.4.5"
category = "database"
logger = "com.zaxxer.hikari"
[dependencies.Ktorm]
[dependencies.ktorm]
groupId = "me.liuwj.ktorm"
artifactId = "ktorm-core"
version = "3.0.0"
category = "database"
logger = "me.liuwj.ktorm.database"
[dependencies.Ktorm-Mysql]
[dependencies.ktorm-mysql]
groupId = "me.liuwj.ktorm"
artifactId = "ktorm-support-mysql"
version = "3.0.0"
version = "ktorm"
category = "database"
[dependencies.junit]
groupId = "org.junit.jupiter"
artifactId = "junit-jupiter"
version = "5.7.0"
scope = "test"
category = "test"
default = true
@@ -139,62 +145,52 @@ default = true
[dependencies.junit-params]
groupId = "org.junit.jupiter"
artifactId = "junit-jupiter-params"
version = "5.7.0"
version = "junit"
scope = "test"
category = "test"
default = true
[dependencies.mokk]
[dependencies.mockk]
groupId = "io.mockk"
artifactId = "mockk"
version = "1.10.0"
scope = "test"
category = "test"
[dependencies.hamkrest]
groupId = "com.natpryce"
artifactId = "hamkrest"
version = "1.7.0.3"
scope = "test"
category = "test"
[dependencies.assertJ]
[dependencies.assertj]
groupId = "org.assertj"
artifactId = "assertj-core"
version = "3.17.2"
scope = "test"
category = "test"
default = true
[dependencies.Kodein-DI]
[dependencies.kodein-di]
groupId = "org.kodein.di"
artifactId = "kodein-di-jvm"
version = "7.0.0"
category = "injection"
repository = "jcenter"
[dependencies.Koin]
[dependencies.koin]
groupId = "org.koin"
artifactId = "koin-core"
version = "2.1.6"
category = "injection"
repository = "jcenter"
default = true
[dependencies.Jackson]
[dependencies.jackson]
groupId = "com.fasterxml.jackson.module"
artifactId = "jackson-module-kotlin"
version = "2.11.2"
category = "serialization"
[dependencies.Kotlinx-serialization]
[dependencies.kotlinx-serialization]
groupId = "org.jetbrains.kotlinx"
artifactId = "kotlinx-serialization-runtime"
version = "1.0-M1-1.4.0-rc"
category = "serialization"
[dependencies.Arrow-core]
[dependencies.arrow-core]
groupId = "io.arrow-kt"
artifactId = "arrow-core"
version = "0.10.5"
repository = "arrow"
+67 -4
View File
@@ -6,8 +6,8 @@
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>14</maven.compiler.target>
<maven.compiler.source>14</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>1.4.10</kotlin.version>
<kotlin.code.style>official</kotlin.code.style>
@@ -40,7 +40,7 @@
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-core</artifactId>
<version>3.260.0</version>
<version>3.265.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@@ -57,6 +57,25 @@
<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.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</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>
<build>
@@ -129,7 +148,7 @@
</execution>
</executions>
<configuration>
<jvmTarget>11</jvmTarget>
<jvmTarget>14</jvmTarget>
</configuration>
</plugin>
<plugin>
@@ -137,6 +156,50 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>ktlint</id>
<phase>verify</phase>
<configuration>
<target name="ktlint">
<java taskname="ktlint" dir="${basedir}" fork="true" failonerror="true"
classname="com.pinterest.ktlint.Main" classpathref="maven.plugin.classpath">
<arg value="src/**/*.kt"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>ktlint-format</id>
<configuration>
<target name="ktlint">
<java taskname="ktlint" dir="${basedir}" fork="true" failonerror="true"
classname="com.pinterest.ktlint.Main" classpathref="maven.plugin.classpath">
<arg value="-F"/>
<arg value="src/**/*.kt"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.pinterest</groupId>
<artifactId>ktlint</artifactId>
<version>0.39.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
-48
View File
@@ -1,48 +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>,
val repositories: List<Repository>,
)
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()
val dependencies = cfg.configMap("dependencies")
.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),
values["logger"],
values["repository"],
)
}
val inputs = cfg.configMap("inputs")
.map { (name, values) ->
Input(name, values["display"], values["default"])
}
val repositories = cfg.configMap("repositories")
.map { (name, values) ->
Repository(name, values["url"])
}
return StarterConfig(dependencies, inputs, repositories)
}
}
+4 -50
View File
@@ -1,58 +1,12 @@
package starter
import org.http4k.core.then
import org.http4k.filter.ServerFilters
import org.http4k.routing.ResourceLoader
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.routes
import org.http4k.routing.static
import org.http4k.server.SunHttp
import org.http4k.server.asServer
import org.koin.core.context.startKoin
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.dsl.onClose
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import starter.routes.IndexRouteSupplier
import starter.routes.RouteSupplier
import starter.routes.ZipRouteSupplier
import starter.routes.toRouter
import starter.templates.*
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()) }
}
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
}
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()
))
}
}
import org.koin.core.context.unloadKoinModules
import starter.modules.*
fun main() {
startKoin {
modules(mainModule, pebbleModule, templateModule, routesModule)
modules(mainModule, pebbleModule, templateModule, configModule, routesModule)
}
unloadKoinModules(configModule)
}
+8 -3
View File
@@ -12,22 +12,27 @@ data class Dependency(
val name: String,
val groupId: String,
val artifactId: String,
val version: String,
val version: Version,
val default: Boolean,
val category: Category,
val scope: Scope,
val logger: String?,
val repository: 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 Feature(val name: String, val value: Boolean = false)
data class Project(
val name: String,
val basePackage: String,
val inputs: List<Input>,
val features: List<Feature>,
val dependencies: List<Dependency>,
val repositories: List<Repository>,
val repositories: Set<Repository>,
)
data class Version(val name: String, val value: String)
+65
View File
@@ -0,0 +1,65 @@
package starter.config
import com.electronwill.nightconfig.core.UnmodifiableConfig
import starter.*
import com.electronwill.nightconfig.core.Config as NightConfig
data class StarterConfig(
val dependencies: List<Dependency>,
val inputs: List<Input>,
val features: List<Feature>,
)
class Config(private val cfg: NightConfig) {
@Suppress("UNCHECKED_CAST")
private fun NightConfig.configMap(key: String) = this.get<NightConfig>(key)
?.valueMap() as Map<String, NightConfig>?
?: emptyMap()
fun load(): StarterConfig {
@Suppress("UNCHECKED_CAST")
val versions = cfg.get<UnmodifiableConfig>("versions")
?.valueMap() as Map<String, String>?
?: emptyMap()
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"] ?: name,
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"])
}
val features = cfg.configMap("features")
.map { (name, values) ->
Feature(name, values["default"] ?: false)
}
return StarterConfig(dependencies, inputs, features)
}
}
@@ -0,0 +1,11 @@
package starter.modules
import com.electronwill.nightconfig.core.file.FileConfig
import org.koin.dsl.module
import starter.config.Config
import com.electronwill.nightconfig.core.Config as NightConfig
val configModule = module {
single<NightConfig> { FileConfig.of("config.toml").apply { load() } }
single { Config(get()).load() }
}
@@ -0,0 +1,22 @@
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.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 { LoggerFactory.getLogger("Starter") }
single { Views(get(), get()) }
single { ProjectZip(getAll()) }
}
@@ -0,0 +1,24 @@
package starter.modules
import org.koin.dsl.bind
import org.koin.dsl.module
import starter.pebble.DepAsXmlPebbleFunction
import starter.pebble.HasFeatureFilter
import starter.utils.PebbleEngineBuilder
import starter.utils.PebbleEngineBuilder.CacheType.ConcurrentMap
import starter.utils.PebbleFilter
import starter.utils.PebbleFunction
val pebbleModule = module {
single { DepAsXmlPebbleFunction() } bind PebbleFunction::class
single { HasFeatureFilter() } bind PebbleFilter::class
single {
PebbleEngineBuilder {
cache = ConcurrentMap
functions(getAll())
filters(getAll())
classPath { suffix = ".twig" }
}
}
}
@@ -0,0 +1,29 @@
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
import starter.utils.ProjectExtractor
import starter.utils.ProjectExtractorImpl
val routesModule = module {
single { IndexRouteSupplier(get()) } bind RouteSupplier::class
single { ZipRouteSupplier(get(), get()) } bind RouteSupplier::class
single<ProjectExtractor> { ProjectExtractorImpl(get()) }
single {
ServerFilters.CatchAll().then(
routes(
static(ResourceLoader.Classpath("/assets")),
getAll<RouteSupplier>().toRouter()
)
)
}
}
@@ -0,0 +1,14 @@
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
single { EditorConfigTemplate(get()) } bind Template::class
}
@@ -1,57 +1,51 @@
package starter
package starter.pebble
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.utils.PebbleEngineBuilder
import starter.utils.PebbleEngineBuilder.CacheType.ConcurrentMap
import starter.Dependency
import starter.utils.PebbleFunction
// We need a custom function since pebble inserts whitespace everywhere
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 {
override fun execute(
args: Map<String, Any>,
self: PebbleTemplate,
context: EvaluationContext,
lineNumber: Int,
): SafeString {
val dep = args["dependency"] as Dependency
fun tagName(name: String) = """<span class="text-red-700">$name</span>"""
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>"""
val result = """<span class="text-gray-700">&lt;</span>""" +
"""${tagName(name)}<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>"""
val result = """<span class="text-gray-700">&lt;/</span>""" +
"""${tagName(name)}<span class="text-gray-700">&gt;</span>"""
return result
}
fun tag(name: String, content: String): String {
return """${startTag(name)}$content${endTag(name)}"""
}
fun tag(name: String, content: String) = """${startTag(name)}$content${endTag(name)}"""
val result = """
|${startTag("dependency")}
| ${tag("groupId", dep.groupId)}
| ${tag("artifactId", dep.artifactId)}
| ${tag("version", dep.version)}
| ${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,27 @@
package starter.pebble
import com.mitchellbosecke.pebble.template.EvaluationContext
import com.mitchellbosecke.pebble.template.PebbleTemplate
import starter.Feature
import starter.utils.PebbleFilter
class HasFeatureFilter : PebbleFilter {
override val name = "hasFeature"
override fun getArgumentNames() = listOf("name")
override fun apply(
input: Any,
args: MutableMap<String, Any>,
self: PebbleTemplate,
context: EvaluationContext,
lineNumber: Int,
): Boolean {
@Suppress("UNCHECKED_CAST")
val features = input as List<Feature>
val name = args["name"] as String
return features.any { it.name == name }
}
}
@@ -4,8 +4,7 @@ import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.routing.bind
import starter.StarterConfig
import starter.Views
import starter.views.Views
class IndexRouteSupplier(private val views: Views) : RouteSupplier {
override fun get() = "/" bind Method.GET to {
@@ -1,56 +1,35 @@
package starter.routes
import org.http4k.core.Method
import org.http4k.core.Request
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.StarterConfig
import starter.extensions.attachment
import starter.extensions.badRequest
import starter.extensions.ok
import starter.utils.ProjectExtractor
import java.io.ByteArrayInputStream
class ZipRouteSupplier(
private val conf: StarterConfig,
private val projectExtractor: ProjectExtractor,
private val projectZip: ProjectZip,
) : RouteSupplier {
override fun get() = "/" bind Method.POST to { req ->
private fun handle(req: Request): Response {
val project = projectExtractor(req) ?: return Response.badRequest()
val deps = conf.dependencies.filter {
req.form(it.name) != null
}
val outputStream = projectZip.createZip(project)
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 = conf.repositories
.filter { repo -> repo.name in deps.mapNotNull { it.repository } }
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"
)
return Response.ok().with(
attachment(
value = ByteArrayInputStream(outputStream.toByteArray()),
name = "${project.name}.zip",
contentType = "application/zip"
)
}
)
}
override fun get() = "/" bind Method.POST to ::handle
}
@@ -0,0 +1,15 @@
package starter.templates
import com.mitchellbosecke.pebble.PebbleEngine
import starter.Project
import starter.utils.render
class EditorConfigTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) =
".editorconfig"
override fun enabled(project: Project) = project.features.any { it.name == "ktlint" }
override fun render(project: Project) =
engine.render("starter/editorconfig/index")
}
@@ -0,0 +1,11 @@
package starter.templates
import com.mitchellbosecke.pebble.PebbleEngine
import starter.Project
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")
}
@@ -6,7 +6,7 @@ import starter.utils.render
class MainTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) =
"src/main/kotlin/" + project.basePackage.replace('.', '/') + "/" + project.name.toLowerCase().capitalize() + ".kt"
"src/main/kotlin/" + project.name.toLowerCase().capitalize() + ".kt"
override fun render(project: Project) =
engine.render("starter/main/index", mapOf("basePackage" to project.basePackage))
@@ -12,7 +12,9 @@ class PomTemplate(private val engine: PebbleEngine) : Template {
val args: MutableMap<String, Any?> = mutableMapOf(
"dependencies" to project.dependencies.sortedBy { it.scope },
"repositories" to project.repositories,
"kotlinxSerialization" to project.dependencies.any { it.name == "Kotlinx-serialization" },
"kotlinxSerialization" to project.dependencies.any { it.name == "kotlinx-serialization" },
"features" to project.features,
"versions" to project.dependencies.map { it.version }.toSet()
)
project.inputs.forEach {
+14 -10
View File
@@ -64,16 +64,20 @@ class PebbleEngineBuilder {
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()
})
.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 }
@@ -0,0 +1,44 @@
package starter.utils
import org.http4k.core.Request
import org.http4k.core.body.form
import org.http4k.core.body.formAsMap
import starter.Project
import starter.config.StarterConfig
interface ProjectExtractor {
operator fun invoke(request: Request): Project?
}
class ProjectExtractorImpl(private val conf: StarterConfig) : ProjectExtractor {
override fun invoke(request: Request): Project? {
val deps = conf.dependencies.filter {
request.form(it.name) != null
}
val formMap = request.formAsMap()
val inputKeys = conf.inputs.map { it.name }
val inputs = formMap
.filter { it.key in inputKeys }
.map { (name, value) ->
conf.inputs.find { it.name == name }!!.copy(value = value.first())
}
val features = formMap
.filter { it.key in conf.features.map { it.name } }
.map { (name, _) ->
conf.features.find { it.name == name }!!.copy(value = true)
}
val projectName = inputs.find { it.name == "name" }?.value ?: return null
val basePackage = inputs.find { it.name == "basePackage" }?.value ?: return null
return if (basePackage.contains("/") || basePackage.contains("..")) {
null
} else {
val repositories = deps.mapNotNull { it.repository }.toSet()
Project(projectName, basePackage, inputs, features, deps, repositories)
}
}
}
@@ -1,12 +1,14 @@
package starter
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
"inputs" to config.inputs,
"features" to config.features,
)
fun index() = engine.render("views/index", args)
@@ -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
@@ -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
@@ -2,15 +2,14 @@
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
{% for dep in dependencies %}
<dependency>
<groupId>{{ dep.groupId }}</groupId>
<artifactId>{{ dep.artifactId }}</artifactId>
<version>{{ dep.version }}</version>
<version>{{ "${" }}{{ dep.version.name }}{{ ".version}" }}</version>
{% if dep.scope == "Test" %}
<scope>test</scope>
<scope>test</scope>
{% endif %}
</dependency>
{% endfor %}
@@ -0,0 +1,11 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-bom</artifactId>
<version>${kotlin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -10,5 +10,7 @@
{% include "starter/pom/plugins/@shade" %}
{% include "starter/pom/plugins/@ktlint" %}
</plugins>
</build>
+9 -1
View File
@@ -8,16 +8,24 @@
<properties>
<java.version>{{ javaVersion }}</java.version>
<kotlin.version>{{ kotlinVersion }}</kotlin.version>
<kotlin.code.style>official</kotlin.code.style>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
<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>
{% include "starter/pom/@dependencies" %}
{% include "starter/pom/@repositories" %}
{% include "starter/pom/@dependencyManagement" %}
{% include "starter/pom/@plugins" %}
</project>
@@ -0,0 +1,46 @@
{% if features | hasFeature("ktlint") %}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>ktlint</id>
<phase>verify</phase>
<configuration>
<target name="ktlint">
<java taskname="ktlint" dir="${basedir}" fork="true" failonerror="true"
classname="com.pinterest.ktlint.Main" classpathref="maven.plugin.classpath">
<arg value="src/**/*.kt"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>ktlint-format</id>
<configuration>
<target name="ktlint">
<java taskname="ktlint" dir="${basedir}" fork="true" failonerror="true"
classname="com.pinterest.ktlint.Main" classpathref="maven.plugin.classpath">
<arg value="-F"/>
<arg value="src/**/*.kt"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.pinterest</groupId>
<artifactId>ktlint</artifactId>
<version>0.39.0</version>
</dependency>
</dependencies>
</plugin>
{% endif %}
+13
View File
@@ -10,6 +10,19 @@
{% endfor %}
<div class="mt-4">
<section>
<h2 class="category">Features</h2>
<ul>
{% for feature in features %}
<label class="m-2">
<input name="{{ feature.name }}"
type="checkbox"{% if feature.value %} checked{% endif %}>
<span>{{ feature.name }}</span>
</label>
{% endfor %}
</ul>
</section>
{% for category in dependencies %}
<section>
<h2 class="category">{{ category.key }}</h2>
+46
View File
@@ -0,0 +1,46 @@
package starter
import starter.config.StarterConfig
val testConfig = StarterConfig(
dependencies = listOf(
Dependency(
name = "h2",
groupId = "com.h2database",
artifactId = "h2",
version = Version("h2", "1.4.200"),
default = false,
category = Category.Database,
scope = Scope.Compile,
logger = null,
repository = null,
),
Dependency(
name = "assertj",
groupId = "org.assertj",
artifactId = "assertj-core",
version = Version("assertj", "3.17.2"),
default = true,
category = Category.Test,
scope = Scope.Test,
logger = null,
repository = null,
),
Dependency(
name = "koin",
groupId = "org.koin",
artifactId = "koin-core",
version = Version("koin", "2.1.6"),
default = true,
category = Category.Injection,
scope = Scope.Compile,
logger = null,
repository = Repository("jcenter", "https://jcenter.bintray.com"),
),
),
inputs = listOf(
Input(name = "name", display = "name", value = "example"),
Input(name = "basePackage", display = "Base package", value = "org.example"),
),
features = listOf(Feature("ktlint", true))
)
@@ -0,0 +1,114 @@
package starter.config
import com.electronwill.nightconfig.toml.TomlParser
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test
import starter.*
import java.io.StringReader
internal class ConfigTest {
private val parser = TomlParser()
private fun parse(config: String) = parser.parse(StringReader(config))
@Test
fun inputs() {
@Language("toml")
val toml = """
[inputs]
[inputs.name]
default = "example"
display = "Project Name"
[inputs.basePackage]
default = "org.example"
display = "Base package"
""".trimIndent()
val starterConfig = Config(parse(toml)).load()
val expected = listOf(
Input("basePackage", "Base package", "org.example"),
Input("name", "Project Name", "example"),
)
assertThat(starterConfig.inputs).containsExactlyInAnyOrderElementsOf(expected)
}
@Test
fun dependencies() {
@Language("toml")
val toml = """
[versions]
assertj = "3.17.2"
koin = "2.1.6"
h2 = "1.4.200"
[repositories]
[repositories.jcenter]
url = "https://jcenter.bintray.com"
[dependencies]
[dependencies.assertj]
groupId = "org.assertj"
artifactId = "assertj-core"
scope = "test"
category = "test"
default = true
[dependencies.koin]
groupId = "org.koin"
artifactId = "koin-core"
category = "injection"
repository = "jcenter"
default = true
[dependencies.h2]
groupId = "com.h2database"
category = "database"
""".trimIndent()
val starterConfig = Config(parse(toml)).load()
val expected = listOf(
Dependency(
name = "h2",
groupId = "com.h2database",
artifactId = "h2",
version = Version("h2", "1.4.200"),
default = false,
category = Category.Database,
scope = Scope.Compile,
logger = null,
repository = null,
),
Dependency(
name = "assertj",
groupId = "org.assertj",
artifactId = "assertj-core",
version = Version("assertj", "3.17.2"),
default = true,
category = Category.Test,
scope = Scope.Test,
logger = null,
repository = null,
),
Dependency(
name = "koin",
groupId = "org.koin",
artifactId = "koin-core",
version = Version("koin", "2.1.6"),
default = true,
category = Category.Injection,
scope = Scope.Compile,
logger = null,
repository = Repository("jcenter", "https://jcenter.bintray.com"),
),
)
assertThat(starterConfig.dependencies).containsExactlyInAnyOrderElementsOf(expected)
}
}
@@ -0,0 +1,120 @@
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.configModule
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(configModule, 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,
features = emptyList(),
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")
}
}
@@ -0,0 +1,109 @@
package starter.utils
import org.assertj.core.api.Assertions.assertThat
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.body.Form
import org.http4k.core.body.toBody
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import org.koin.dsl.koinApplication
import org.koin.dsl.module
import starter.Feature
import starter.Input
import starter.Project
import starter.modules.routesModule
import starter.testConfig
import java.util.stream.Stream
internal class ProjectExtractorImplTest {
private val fakeModule = module {
single { testConfig }
}
private val koin = koinApplication {
modules(routesModule, fakeModule)
}.koin
private val projectExtractor = koin.get<ProjectExtractor>()
@Suppress("unused")
fun invalidProjectsSource(): Stream<Form> = Stream.of(
emptyList(),
listOf(
"basePackage" to "org.example",
),
listOf(
"basePackage" to "org.example/a",
"name" to "test"
),
)
@ParameterizedTest
@MethodSource("invalidProjectsSource")
fun invalidProjects(form: Form) {
val request = Request(Method.POST, "")
.body(form.toBody())
val project = projectExtractor(request)
assertThat(project).isNull()
}
@Suppress("unused")
fun validProjectsSource(): Stream<Pair<Form, Project>> = Stream.of(
listOf(
"name" to "Test",
"basePackage" to "org.example",
"ktlint" to "",
) to Project(
name = "Test",
basePackage = "org.example",
inputs = listOf(
Input(name = "name", display = "name", value = "Test"),
Input(name = "basePackage", display = "Base package", value = "org.example"),
),
features = listOf(
Feature(name = "ktlint", value = true)
),
dependencies = listOf(),
repositories = setOf(),
),
listOf(
"name" to "Test",
"basePackage" to "org.example",
"ktlint" to "",
"koin" to "",
) to Project(
name = "Test",
basePackage = "org.example",
inputs = listOf(
Input(name = "name", display = "name", value = "Test"),
Input(name = "basePackage", display = "Base package", value = "org.example"),
),
features = listOf(
Feature(name = "ktlint", value = true)
),
dependencies = listOf(
testConfig.dependencies.find { it.name == "koin" }!!
),
repositories = setOf(
testConfig.dependencies.find { it.name == "koin" }!!.repository!!
),
),
)
@ParameterizedTest
@MethodSource("validProjectsSource")
fun validProjects(pair: Pair<Form, Project>) {
val (form, expected) = pair
val request = Request(Method.POST, "")
.body(form.toBody())
val project = projectExtractor(request)
assertThat(project).isEqualTo(expected)
}
}
@@ -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