Skip to content

Commit

Permalink
feat: add support for freemarker and binary files
Browse files Browse the repository at this point in the history
feat: add `--name` and `--package` options to `create` command

test: update unit test for `blank` template

test: add unit test for `cli` template
  • Loading branch information
mayekukhisa committed Jun 1, 2024
1 parent f89d258 commit cc0bb8b
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 21 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ repositories {
dependencies {
implementation(libs.clikt)
implementation(libs.commons.io)
implementation(libs.freemarker)
implementation(libs.kotlinx.serialization.json)
testImplementation(libs.kotlin.test)
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
[versions]
commons-io = "2.16.1"
clikt = "4.4.0"
freemarker = "2.3.32"
kotlin = "1.9.24"
kotlinx-serialization = "1.6.3"
spotless = "6.25.0"

[libraries]
commons-io = { group = "commons-io", name = "commons-io", version.ref = "commons-io" }
clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt" }
freemarker = { group = "org.freemarker", name = "freemarker", version.ref = "freemarker" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/mayekukhisa/scaffold/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class App : CliktCommand(
}

context {
helpFormatter = { MordantHelpFormatter(it, requiredOptionMarker = "*") }
helpFormatter = { MordantHelpFormatter(it, requiredOptionMarker = "*", showDefaultValues = true) }
}
}

Expand Down
94 changes: 79 additions & 15 deletions src/main/kotlin/com/mayekukhisa/scaffold/command/Create.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,25 @@ import com.github.ajalt.clikt.core.PrintMessage
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.defaultLazy
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.options.validate
import com.github.ajalt.clikt.parameters.types.file
import com.mayekukhisa.scaffold.App
import com.mayekukhisa.scaffold.BuildConfig
import com.mayekukhisa.scaffold.model.TemplateFile
import com.mayekukhisa.scaffold.model.TemplateManifest
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.StringWriter
import java.util.Locale
import freemarker.template.Configuration as Freemarker

class Create : CliktCommand(
help = "Generate a new project from a template",
Expand All @@ -47,6 +54,27 @@ class Create : CliktCommand(
.convert { name -> App.templates.find { it.name == name } ?: fail(name) }
.required()

private val projectName by option(
"-n",
"--name",
metavar = "name",
help = "The name to assign to the project",
)
.defaultLazy("project's directory name") { projectDir.name }

private val packageName by option(
"-p",
"--package",
metavar = "package",
help = "The base package for the project",
)
.default("com.example")
.validate {
if (!it.matches(Regex("^[a-z]+(\\.[a-z]+)*$"))) {
fail("Invalid package")
}
}

private val projectDir by argument(
name = "directory",
help = "The directory to create the project in",
Expand All @@ -56,6 +84,14 @@ class Create : CliktCommand(

private val templateCollectionDir by lazy { File(App.config.getProperty("template.collection.path")) }

private val freemarker by lazy {
Freemarker(Freemarker.VERSION_2_3_32).apply {
setDirectoryForTemplateLoading(templateCollectionDir.resolve(projectTemplate.path))
defaultEncoding = Charsets.UTF_8.name()
locale = Locale.US
}
}

override fun run() {
if (!projectDir.mkdirs()) {
throw PrintMessage(
Expand All @@ -67,24 +103,14 @@ class Create : CliktCommand(

try {
with(Json { ignoreUnknownKeys = true }) {
val jsonString =
FileUtils.readFileToString(
templateCollectionDir.resolve("${projectTemplate.path}/manifest.json"),
Charsets.UTF_8,
)
val jsonString = freemarker.processTemplateFile("manifest.json")

decodeFromString<TemplateManifest>(jsonString).run {
echo("Generating project structure...")
textFiles.forEach {
try {
FileUtils.copyFile(
templateCollectionDir.resolve("${projectTemplate.path}/${it.sourcePath}"),
projectDir.resolve(it.targetPath),
)
} catch (e: FileNotFoundException) {
abortProjectGeneration("Error: Template file '${it.sourcePath}' not found")
}
}

binaryFiles.forEach { generateProjectFile(it) }
freemarkerFiles.forEach { generateProjectFile(it, preprocess = true) }
textFiles.forEach { generateProjectFile(it) }
}
}
} catch (e: IOException) {
Expand All @@ -96,6 +122,44 @@ class Create : CliktCommand(
echo("Done!")
}

private fun Freemarker.processTemplateFile(filepath: String): String {
val dataModel =
mapOf(
"projectName" to projectName,
"packageName" to packageName,
)
return with(StringWriter()) {
getTemplate(filepath).process(dataModel, this)
toString()
}
}

private fun generateProjectFile(
templateFile: TemplateFile,
preprocess: Boolean = false,
) {
try {
val outputFile = projectDir.resolve(templateFile.targetPath)

if (preprocess) {
FileUtils.writeStringToFile(
outputFile,
freemarker.processTemplateFile(templateFile.sourcePath),
Charsets.UTF_8,
)
} else {
FileUtils.copyFile(
templateCollectionDir.resolve("${projectTemplate.path}/${templateFile.sourcePath}"),
outputFile,
)
}

outputFile.setExecutable(templateFile.executable, false)
} catch (e: FileNotFoundException) {
abortProjectGeneration("Error: Template file '${templateFile.sourcePath}' not found")
}
}

private fun abortProjectGeneration(errorMessage: String) {
FileUtils.deleteDirectory(projectDir)
throw PrintMessage(errorMessage, statusCode = 1, printError = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ import kotlinx.serialization.Serializable
data class TemplateFile(
val sourcePath: String,
val targetPath: String,
val executable: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ import kotlinx.serialization.Serializable

@Serializable
data class TemplateManifest(
val binaryFiles: List<TemplateFile>,
val freemarkerFiles: List<TemplateFile>,
val textFiles: List<TemplateFile>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,19 @@ class CreateTest {
checkProjectStructure(projectDir)
}

@Test
fun `should create cli project`() {
val projectDir = tempDir.resolve("cli-project")
Create().parse(arrayOf("--template", "cli", "$projectDir"))
checkProjectStructure(projectDir)
}

private fun checkProjectStructure(projectDir: File) {
val bashPath = App.config.getProperty("bash.path") ?: error("Error: Bash path not set")

val bashProcess =
Runtime.getRuntime().exec(bashPath, null, projectDir).apply {
val command = "find . -type f -exec stat -c \"%n\" {} \\; | LC_ALL=C sort"
val command = "find . -type f -exec stat -c \"%a %n\" {} \\; | LC_ALL=C sort"
outputStream.use { IOUtils.write(command, it, Charsets.UTF_8) }
waitFor()
}
Expand Down
8 changes: 4 additions & 4 deletions src/test/resources/project-structures/blank-project.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
./.editorconfig
./.gitattributes
./.gitignore
./.vscode/settings.json
644 ./.editorconfig
644 ./.gitattributes
644 ./.gitignore
644 ./.vscode/settings.json
16 changes: 16 additions & 0 deletions src/test/resources/project-structures/cli-project.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
644 ./.editorconfig
644 ./.gitattributes
644 ./.gitignore
644 ./.vscode/settings.json
644 ./build.gradle.kts
644 ./gradle.properties
644 ./gradle/libs.versions.toml
644 ./gradle/wrapper/gradle-wrapper.jar
644 ./gradle/wrapper/gradle-wrapper.properties
644 ./gradlew.bat
644 ./settings.gradle.kts
644 ./spotless/config/prettierrc.json
644 ./src/main/kotlin/com/example/App.kt
644 ./src/main/kotlin/com/example/Main.kt
644 ./src/test/kotlin/com/example/ExampleUnitTest.kt
755 ./gradlew

0 comments on commit cc0bb8b

Please sign in to comment.