1
0

Compare commits

...

4 Commits

Author SHA1 Message Date
cf09799bc6 css 2020-10-06 15:42:13 +02:00
a856d5e425 Display dependency fragments 2020-10-06 15:27:38 +02:00
f56ec93498 Update dependencies 2020-10-05 17:42:11 +02:00
66878900f5 Clean + lint 2020-09-30 02:39:58 +02:00
24 changed files with 388 additions and 186 deletions

18
.editorconfig Normal file
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

View File

@ -29,48 +29,47 @@ url = "https://dl.bintray.com/arrow-kt/arrow-kt/"
[dependencies.http4k]
groupId = "org.http4k"
artifactId = "http4k-core"
version = "3.261.0"
category = "server"
version = "3.264.0"
category = "http4k"
default = true
[dependencies.http4k-server-jetty]
groupId = "org.http4k"
artifactId = "http4k-server-jetty"
version = "3.261.0"
category = "server"
version = "3.264.0"
category = "http4k"
default = true
logger = "org.eclipse.jetty"
[dependencies.http4k-server-apache]
groupId = "org.http4k"
artifactId = "http4k-server-apache"
version = "3.261.0"
category = "server"
version = "3.264.0"
category = "http4k"
[dependencies.http4k-client-apache]
groupId = "org.http4k"
artifactId = "http4k-client-apache"
version = "3.261.0"
category = "server"
version = "3.264.0"
category = "http4k"
[dependencies.http4k-format-jackson]
groupId = "org.http4k"
artifactId = "http4k-format-jackson"
version = "3.261.0"
category = "server"
version = "3.264.0"
category = "http4k"
[dependencies.http4k-format-kotlinx-serialization]
groupId = "org.http4k"
artifactId = "http4k-format-kotlinx-serialization"
version = "3.264.0"
category = "http4k"
[dependencies.http4k-contract]
groupId = "org.http4k"
artifactId = "http4k-contract"
version = "3.261.0"
category = "server"
[dependencies.javalin]
groupId = "io.javalin"
artifactId = "javalin"
version = "3.10.1"
category = "server"
logger = "org.eclipse.jetty"
version = "3.264.0"
category = "http4k"
[dependencies.pebble]
groupId = "io.pebbletemplates"

View File

@ -10,6 +10,7 @@
<maven.compiler.source>11</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>
</properties>
<repositories>
@ -51,6 +52,11 @@
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
<build>

View File

@ -1,7 +1,8 @@
module.exports = {
purge: {
content: [
'../main/resources/views/**/*.twig'
'../main/resources/views/**/*.twig',
'../main/kotlin/starter/PebbleModule.kt',
]
},
theme: {},

View File

@ -4,9 +4,9 @@ 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>,
val dependencies: List<Dependency>,
val inputs: List<Input>,
val repositories: List<Repository>,
)
class Config {
@ -19,31 +19,30 @@ class Config {
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"],
)
}
.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"])
}
.map { (name, values) ->
Input(name, values["display"], values["default"])
}
val repositories = cfg.configMap("repositories")
.map { (name, values) ->
Repository(name, values["url"])
}
.map { (name, values) ->
Repository(name, values["url"])
}
return StarterConfig(dependencies, inputs, repositories)
}
}

View File

@ -1,15 +1,34 @@
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 { PebbleModule().engine() }
single { Server(get(), get(), get()) }
single { Views(get()) }
single { LoggerFactory.getLogger("Starter") }
single { Views(get(), get()) }
single { ProjectZip(getAll()) }
}
@ -20,10 +39,20 @@ val templateModule = module {
single { GitignoreTemplate(get()) } bind Template::class
}
fun main() {
val koin = startKoin {
modules(mainModule, templateModule)
}.koin
val server = koin.get<Server>()
server.run()
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()
))
}
}
fun main() {
startKoin {
modules(mainModule, pebbleModule, templateModule, routesModule)
}
}

View File

@ -1,7 +1,7 @@
package starter
enum class Category {
Server, Injection, Database, Serialization, Test, Other
Http4k, Injection, Database, Serialization, Test, Other
}
enum class Scope {
@ -9,15 +9,15 @@ enum class Scope {
}
data class Dependency(
val name: String,
val groupId: String,
val artifactId: String,
val version: String,
val default: Boolean,
val category: Category,
val scope: Scope,
val logger: String?,
val repository: String?,
val name: String,
val groupId: String,
val artifactId: String,
val version: String,
val default: Boolean,
val category: Category,
val scope: Scope,
val logger: String?,
val repository: String?,
)
data class Repository(val name: String, val url: String)
@ -25,9 +25,9 @@ data class Repository(val name: String, val url: String)
data class Input(val name: String, val display: String, val value: String? = null)
data class Project(
val name: String,
val basePackage: String,
val inputs: List<Input>,
val dependencies: List<Dependency>,
val repositories: List<Repository>,
val name: String,
val basePackage: String,
val inputs: List<Input>,
val dependencies: List<Dependency>,
val repositories: List<Repository>,
)

View File

@ -1,15 +1,57 @@
package starter
import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.loader.ClasspathLoader
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.utils.PebbleFunction
class PebbleModule {
fun engine(): PebbleEngine {
val loader = ClasspathLoader()
loader.suffix = ".twig"
return PebbleEngine.Builder()
.loader(loader)
.cacheActive(true)
.build()
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)}
|${endTag("dependency")}
""".trimMargin()
return SafeString(result)
}
}
val pebbleModule = module {
single { DepAsXmlPebbleFunction() } bind PebbleFunction::class
single {
PebbleEngineBuilder {
cache = ConcurrentMap
functions(getAll())
classPath { suffix = ".twig" }
}
}
}

View File

@ -17,5 +17,4 @@ class ProjectZip(private val templates: List<Template>) {
}
return zipOutput.outputStream
}
}

View File

@ -1,68 +0,0 @@
package starter
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.body.form
import org.http4k.core.body.formAsMap
import org.http4k.routing.ResourceLoader
import org.http4k.routing.bind
import org.http4k.routing.routes
import org.http4k.routing.static
import org.http4k.server.SunHttp
import org.http4k.server.asServer
import starter.utils.sanitizeFilename
import java.io.ByteArrayInputStream
class Server(
private val views: Views,
private val conf: StarterConfig,
private val projectZip: ProjectZip,
) {
fun run() {
val indexHandler: HttpHandler = {
Response(Status.OK).body(views.index(conf.dependencies, conf.inputs)).header("Content-Type", "text/html")
}
val zipHandler: HttpHandler = { 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(Status.BAD_REQUEST).body("Invalid Base Package")
} 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(Status.OK).header("Content-Type", "application/zip")
.header("Content-Disposition", "attachment; filename=\"${sanitizeFilename(projectName)}.zip\"")
.body(ByteArrayInputStream(outputStream.toByteArray()))
}
}
val app = routes(
"/" bind Method.GET to indexHandler,
"/" bind Method.POST to zipHandler,
static(ResourceLoader.Classpath("/assets"))
)
app.asServer(SunHttp(7000)).start()
println("Started on http://localhost:7000")
}
}

View File

@ -1,17 +1,13 @@
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)
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(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)
)
}
}
fun index() = engine.render("views/index", args)
}

View File

@ -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)
}

View File

@ -0,0 +1,16 @@
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.StarterConfig
import starter.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")
}
}

View File

@ -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())

View File

@ -0,0 +1,56 @@
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.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 = 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"
)
)
}
}
}

View File

@ -6,8 +6,8 @@ import starter.utils.render
class GitignoreTemplate(private val engine: PebbleEngine) : Template {
override fun path(project: Project) =
".gitignore"
".gitignore"
override fun render(project: Project) =
engine.render("starter/gitignore/index")
engine.render("starter/gitignore/index")
}

View File

@ -12,7 +12,7 @@ class LogbackTemplate(private val engine: PebbleEngine) : Template {
override fun render(project: Project): String {
val args = mapOf(
"loggers" to project.dependencies.mapNotNull { it.logger }.toSet()
"loggers" to project.dependencies.mapNotNull { it.logger }.toSet()
)
val rendered = engine.render("starter/logback/index", args)
return prettyPrintXml(rendered)

View File

@ -6,8 +6,8 @@ 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.basePackage.replace('.', '/') + "/" + project.name.toLowerCase().capitalize() + ".kt"
override fun render(project: Project) =
engine.render("starter/main/index", mapOf("basePackage" to project.basePackage))
engine.render("starter/main/index", mapOf("basePackage" to project.basePackage))
}

View File

@ -10,9 +10,9 @@ class PomTemplate(private val engine: PebbleEngine) : Template {
override fun render(project: Project): String {
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" },
"dependencies" to project.dependencies.sortedBy { it.scope },
"repositories" to project.repositories,
"kotlinxSerialization" to project.dependencies.any { it.name == "Kotlinx-serialization" },
)
project.inputs.forEach {

View File

@ -1,11 +1,87 @@
package starter.utils
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
fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()): String {
val template = getTemplate(name)
val writer = StringWriter()
template.evaluate(writer, args)
return writer.toString()
}
fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()) =
getTemplate(name).let { StringWriter().apply { it.evaluate(this, args) }.toString() }
fun PebbleEngine.render(name: String, vararg args: Pair<String, Any?>) =
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()
}
}
}

View File

@ -13,22 +13,23 @@ import javax.xml.transform.stream.StreamResult
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
/*
@see https://stackoverflow.com/a/33541820
*/
fun prettyPrintXml(xml: String, indent: Int = 4): String {
// Turn xml string into a document
val document: Document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(InputSource(ByteArrayInputStream(xml.encodeToByteArray())))
.newDocumentBuilder()
.parse(InputSource(ByteArrayInputStream(xml.encodeToByteArray())))
// Remove whitespaces outside tags
document.normalize()
val xPath = XPathFactory.newInstance().newXPath()
val nodeList = xPath.evaluate("//text()[normalize-space()='']",
document,
XPathConstants.NODESET) as NodeList
val nodeList = xPath.evaluate(
"//text()[normalize-space()='']",
document,
XPathConstants.NODESET
) as NodeList
for (i in 0 until nodeList.length) {
val node = nodeList.item(i)
node.parentNode.removeChild(node)
@ -46,4 +47,4 @@ fun prettyPrintXml(xml: String, indent: Int = 4): String {
val stringWriter = StringWriter()
transformer.transform(DOMSource(document), StreamResult(stringWriter))
return stringWriter.toString()
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,14 @@
{% macro dependency(dependency) %}
<label class="m-2">
<input name="{{ dependency.name }}"
type="checkbox"{% if dependency.default %} checked{% endif %}>
<span>{{ dependency.name }}</span>
</label>
<details>
<summary>
<label class="m-2">
<input name="{{ dependency.name }}"
type="checkbox"{% if dependency.default %} checked{% endif %}>
<span>{{ dependency.name }}</span>
</label>
</summary>
<pre><code>{{ depAsXml(dependency) }}</code></pre>
</details>
{% endmacro %}
{% macro input(input) %}
@ -17,4 +22,4 @@
<input name="{{ input.name }}" id="{{ input.name }}" class="input"{% if input.value %} value="{{ input.value }}"{% endif %}>
</div>
</div>
{% endmacro %}
{% endmacro %}

View File

@ -21,6 +21,6 @@
</section>
{% endfor %}
</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>
{% endblock %}