feat(templates): add base configuration for creating templates

This commit is contained in:
Akash Yadav
2023-05-08 23:53:24 +05:30
parent 857158393a
commit 98d1597157
14 changed files with 313 additions and 120 deletions
+3
View File
@@ -124,6 +124,9 @@
<module name="AndroidIDE.subprojects.jsonrpc.test" target="11" />
<module name="AndroidIDE.subprojects.xml-formatter.main" target="11" />
<module name="AndroidIDE.subprojects.xml-formatter.test" target="11" />
<module name="AndroidIDE.templates-testing" target="17" />
<module name="AndroidIDE.templates-testing.main" target="11" />
<module name="AndroidIDE.templates-testing.test" target="11" />
<module name="AndroidIDE.testing-android" target="17" />
<module name="AndroidIDE.treesitter.test" target="1.7" />
</bytecodeTargetLevel>
+3 -10
View File
@@ -32,15 +32,8 @@ dependencies {
implementation(projects.resources)
implementation(projects.subprojects.xmlUtils)
implementation(libs.androidx.annotation)
implementation(libs.aapt2.common)
testImplementation(projects.lsp.api)
testImplementation(projects.preferences)
testImplementation(projects.shared)
testImplementation(libs.tests.androidx.test.core)
testImplementation(libs.tests.google.truth)
testImplementation(libs.tests.junit)
testImplementation(libs.tests.mockk)
testImplementation(libs.tests.robolectric)
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.google.material)
}
@@ -24,6 +24,7 @@ import com.itsaky.androidide.templates.SrcSet
import com.itsaky.androidide.templates.base.modules.android.buildGradleSrc
import com.itsaky.androidide.templates.base.modules.android.proguardRules
import com.itsaky.androidide.templates.base.util.AndroidManifestBuilder
import com.squareup.javapoet.TypeSpec
import java.io.File
class AndroidModuleTemplateBuilder : ModuleTemplateBuilder() {
@@ -69,6 +70,22 @@ class AndroidModuleTemplateBuilder : ModuleTemplateBuilder() {
copyAssetsRecursively(baseAsset("res"), mainResDir())
}
/**
* Creates a new activity class in the application/library package.
*
* @param name The name of the class.
*/
fun RecipeExecutor.createActivity(name: String = "MainActivity",
configure: TypeSpec.Builder.() -> Unit
) {
sources {
createClass(data.packageName, name) {
configure()
}
}
}
override fun baseAsset(path: String): String {
return super.baseAsset("android/${path}")
}
@@ -17,6 +17,7 @@
package com.itsaky.androidide.templates.base
import com.itsaky.androidide.templates.Language
import com.itsaky.androidide.templates.ModuleTemplate
import com.itsaky.androidide.templates.ModuleTemplateData
import com.itsaky.androidide.templates.RecipeExecutor
@@ -25,8 +26,7 @@ import com.itsaky.androidide.templates.TemplateBuilder
import com.itsaky.androidide.templates.TemplateRecipe
import com.itsaky.androidide.templates.base.models.Dependency
import com.itsaky.androidide.templates.base.models.defaultDependency
import com.itsaky.androidide.templates.base.modules.android.proguardRules
import com.itsaky.androidide.templates.base.util.JavaSourceBuilder
import com.itsaky.androidide.templates.base.util.SourceWriter
import java.io.File
/**
@@ -39,7 +39,7 @@ abstract class ModuleTemplateBuilder :
ExecutorDataTemplateBuilder<ModuleTemplate, ModuleTemplateData>() {
internal val dependencies = hashSetOf<Dependency>()
protected val javaSourceBuilder = JavaSourceBuilder()
protected val sourceWriter = SourceWriter()
internal var _name: String? = null
val name: String
@@ -54,7 +54,8 @@ abstract class ModuleTemplateBuilder :
* @param path The path for the asset.
* @see com.itsaky.androidide.templates.base.baseAsset
*/
open fun baseAsset(path: String) = com.itsaky.androidide.templates.base.util.baseAsset("module", path)
open fun baseAsset(path: String) =
com.itsaky.androidide.templates.base.util.baseAsset("module", path)
/**
* Get the `build.gradle[.kts]` file for this module.l
@@ -63,6 +64,18 @@ abstract class ModuleTemplateBuilder :
return data.buildGradleFile()
}
/**
* Get the path to the Java source file in the given [source set][srcSet] with the
* given [packageName] and the [simple name][name].
*/
fun srcFilePath(srcSet: SrcSet, packageName: String, name: String, language: Language): File {
var path = packageName.replace('.', '/')
path += "/${name}"
path += ".${language.ext}"
return File(javaSrc(srcSet), path)
}
/**
* Get the `java` sources directory for the [SrcSet.Main] source set.
*/
@@ -101,12 +114,12 @@ abstract class ModuleTemplateBuilder :
}
/**
* Configure the Java source files for this module.
* Configure the source files for this module.
*
* @param configure Function for configuring the Java source files.
* @param configure Function for configuring the source files.
*/
fun java(configure: JavaSourceBuilder.() -> Unit) {
javaSourceBuilder.apply(configure)
fun sources(configure: SourceWriter.() -> Unit) {
sourceWriter.apply(configure)
}
/**
@@ -141,11 +154,6 @@ abstract class ModuleTemplateBuilder :
*/
fun commonPostRecipe(extraConfig: TemplateRecipe = {}): TemplateRecipe = {
// Write the java source files
javaSourceBuilder.apply {
write()
}
// Write build.gradle[.kts]
buildGradle()
@@ -17,6 +17,7 @@
package com.itsaky.androidide.templates.base.modules.android
import com.itsaky.androidide.templates.Language
import com.itsaky.androidide.templates.ModuleType
import com.itsaky.androidide.templates.base.AndroidModuleTemplateBuilder
import com.itsaky.androidide.templates.base.modules.dependencies
@@ -34,81 +35,95 @@ fun AndroidModuleTemplateBuilder.buildGradleSrc(): String {
private fun AndroidModuleTemplateBuilder.buildGradleSrcKts(): String {
return """
plugins {
id("$androidPlugin")
}
id("$androidPlugin")
${ktPlugin()}
}
android {
compileSdkVersion(${data.versions.compileSdk.api})
buildToolsVersion = "${data.versions.buildTools}"
android {
compileSdkVersion(${data.versions.compileSdk.api})
buildToolsVersion = "${data.versions.buildTools}"
defaultConfig {
applicationId = "${data.packageName}"
minSdk = ${data.versions.minSdk.api}
targetSdk = ${data.versions.targetSdk.api}
versionCode = 1
versionName = "1.0"
}
defaultConfig {
applicationId = "${data.packageName}"
minSdk = ${data.versions.minSdk.api}
targetSdk = ${data.versions.targetSdk.api}
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility ${data.versions.javaSource()}
targetCompatibility ${data.versions.javaTarget()}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
buildFeatures {
viewBinding true
compileOptions {
sourceCompatibility ${data.versions.javaSource()}
targetCompatibility ${data.versions.javaTarget()}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
${dependencies()}
buildFeatures {
viewBinding true
}
}
""".trimIndent()
${dependencies()}
"""
}
private fun AndroidModuleTemplateBuilder.buildGradleSrcGroovy(): String {
return """
plugins {
id '$androidPlugin'
plugins {
id '$androidPlugin'
${ktPlugin()}
}
android {
compileSdk ${data.versions.compileSdk.api}
buildToolsVersion "${data.versions.buildTools}"
defaultConfig {
applicationId "com.itsaky.myapplication"
minSdk ${data.versions.minSdk.api}
targetSdk ${data.versions.targetSdk.api}
versionCode 1
versionName "1.0"
}
android {
compileSdk ${data.versions.compileSdk.api}
buildToolsVersion "${data.versions.buildTools}"
defaultConfig {
applicationId "com.itsaky.myapplication"
minSdk ${data.versions.minSdk.api}
targetSdk ${data.versions.targetSdk.api}
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility ${data.versions.javaSource()}
targetCompatibility ${data.versions.javaTarget()}
}
buildFeatures {
viewBinding true
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
dependencies {
compileOptions {
sourceCompatibility ${data.versions.javaSource()}
targetCompatibility ${data.versions.javaTarget()}
}
""".trimIndent()
}
buildFeatures {
viewBinding true
}
}
${dependencies()}
"""
}
private fun AndroidModuleTemplateBuilder.ktPlugin(): String {
if (data.language != Language.Kotlin) {
return ""
}
return if (data.useKts) ktPluginKts() else ktPluginGroovy()
}
private fun ktPluginKts(): String {
return """id("kotlin-android")"""
}
private fun ktPluginGroovy(): String {
return "id 'kotlin-android'"
}
@@ -31,9 +31,12 @@ internal fun ModuleTemplateBuilder.dependencies(): String {
return StringBuilder().apply {
append("dependencies {")
append(System.lineSeparator())
for (dep in dependencies) {
append(System.lineSeparator())
append(" ${dep.value()}")
}
append(System.lineSeparator())
append("}")
}.toString()
}
@@ -17,6 +17,8 @@
package com.itsaky.androidide.templates.base.util
import com.itsaky.androidide.templates.Language
import com.itsaky.androidide.templates.SrcSet
import com.itsaky.androidide.templates.base.ModuleTemplateBuilder
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
@@ -24,11 +26,11 @@ import com.squareup.javapoet.TypeSpec
import jdkx.lang.model.element.Modifier
/**
* Utility for building Java source files.
* Utility for building/writing source files.
*
* @author Akash Yadav
*/
class JavaSourceBuilder {
class SourceWriter {
/**
* The type of Java source. A [SourceType] creates a [TypeSpec.Builder] and optionally
@@ -82,8 +84,6 @@ class JavaSourceBuilder {
}
}
private val files = hashSetOf<JavaFile>()
/**
* Creates a new Java class.
*
@@ -91,7 +91,9 @@ class JavaSourceBuilder {
* @param className The name of the class.
* @param configure Function to configure the [TypeSpec.Builder].
*/
fun createClass(packageName: String, className: String, configure: TypeSpec.Builder.() -> Unit) {
fun ModuleTemplateBuilder.createClass(packageName: String, className: String,
configure: TypeSpec.Builder.() -> Unit
) {
return createJavaFile(packageName, className, ClassType(), configure)
}
@@ -102,7 +104,9 @@ class JavaSourceBuilder {
* @param className The name of the class.
* @param configure Function to configure the [TypeSpec.Builder].
*/
fun createEnum(packageName: String, className: String, configure: TypeSpec.Builder.() -> Unit) {
fun ModuleTemplateBuilder.createEnum(packageName: String, className: String,
configure: TypeSpec.Builder.() -> Unit
) {
return createJavaFile(packageName, className, EnumType(), configure)
}
@@ -113,7 +117,8 @@ class JavaSourceBuilder {
* @param className The name of the interface.
* @param configure Function to configure the [TypeSpec.Builder].
*/
fun createInterface(packageName: String, className: String, configure: TypeSpec.Builder.() -> Unit
fun ModuleTemplateBuilder.createInterface(packageName: String, className: String,
configure: TypeSpec.Builder.() -> Unit
) {
return createJavaFile(packageName, className, InterfaceType(), configure)
}
@@ -125,8 +130,8 @@ class JavaSourceBuilder {
* @param className The name of the interface.
* @param configure Function to configure the [TypeSpec.Builder].
*/
fun createAnnotation(packageName: String, className: String,
configure: TypeSpec.Builder.() -> Unit
fun ModuleTemplateBuilder.createAnnotation(packageName: String, className: String,
configure: TypeSpec.Builder.() -> Unit
) {
return createJavaFile(packageName, className, AnnotationType(), configure)
}
@@ -139,8 +144,8 @@ class JavaSourceBuilder {
* @param type The type of the class.
* @param configure Function to configure the [TypeSpec.Builder].
*/
fun createJavaFile(packageName: String, className: String, type: SourceType,
configure: TypeSpec.Builder.() -> Unit
fun ModuleTemplateBuilder.createJavaFile(packageName: String, className: String, type: SourceType,
configure: TypeSpec.Builder.() -> Unit
) {
val klass = ClassName.get(packageName, className)
val builder = type.builder(klass).apply(configure).also {
@@ -150,12 +155,62 @@ class JavaSourceBuilder {
}
val file = JavaFile.builder(packageName, builder.build())
file.skipJavaLangImports(true)
files.add(file.build())
write(file.build())
}
internal fun ModuleTemplateBuilder.write() {
for (file in files) {
file.writeTo(mainJavaSrc())
}
/**
* Writes the contents to a new Java file.
*
* @param packageName The package name of the class.
* @param className The name of the class.
* @param source The source code for the file.
*/
fun ModuleTemplateBuilder.writeJavaSrc(packageName: String, className: String,
srcSet: SrcSet = SrcSet.Main, source: String
) {
executor.save(source, srcFilePath(srcSet, packageName, className, Language.Java))
}
/**
* Writes the contents to a new Kotlin file.
*
* @param packageName The package name of the class.
* @param className The name of the class.
* @param source The source code for the file.
*/
fun ModuleTemplateBuilder.writeKtSrc(packageName: String, className: String,
srcSet: SrcSet = SrcSet.Main, source: String
) {
executor.save(source, srcFilePath(srcSet, packageName, className, Language.Kotlin))
}
/**
* Writes the contents to a new Java file.
*
* @param packageName The package name of the class.
* @param className The name of the class.
* @param source A function which returns the source code for the file.
*/
fun ModuleTemplateBuilder.writeJavaSrc(packageName: String, className: String,
srcSet: SrcSet = SrcSet.Main, source: () -> String
) {
writeJavaSrc(packageName, className, srcSet, source())
}
/**
* Writes the contents to a new Kotlin file.
*
* @param packageName The package name of the class.
* @param className The name of the class.
* @param source A function which returns the source code for the file.
*/
fun ModuleTemplateBuilder.writeKtSrc(packageName: String, className: String,
srcSet: SrcSet = SrcSet.Main, source: () -> String
) {
writeKtSrc(packageName, className, srcSet, source())
}
private fun ModuleTemplateBuilder.write(file: JavaFile) {
file.writeTo(mainJavaSrc())
}
}
@@ -19,9 +19,6 @@ package com.itsaky.androidide.templates.base.util
import com.itsaky.androidide.templates.BaseTemplateData
import com.itsaky.androidide.templates.ProjectTemplateData
import com.itsaky.androidide.templates.base.AndroidModuleTemplateBuilder
import com.itsaky.androidide.templates.base.ModuleTemplateBuilder
import com.itsaky.androidide.templates.base.ProjectTemplateBuilder
import java.io.File
/**
@@ -39,7 +36,13 @@ internal fun ProjectTemplateData.moduleNameToDir(name: String): File {
return File(this.projectDir, moduleNameToDirName(name).replace(':', '/').trim { it == '/' })
}
internal fun moduleNameToDirName(name: String): String {
/**
* Converts a Gradle module name to a directory name.
*
* @param name The module name.
* @return The directory name.
*/
fun moduleNameToDirName(name: String): String {
val result = StringBuilder()
var prev: Char? = null
for (i in name.indices) {
+9 -3
View File
@@ -15,8 +15,6 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
@@ -39,6 +37,14 @@ dependencies {
implementation(libs.androidx.ktx)
implementation(libs.google.auto.service.annotations)
testImplementation(libs.tests.junit)
testImplementation(projects.common)
testImplementation(projects.templatesApi)
testImplementation(projects.lsp.api)
testImplementation(projects.preferences)
testImplementation(projects.shared)
testImplementation(libs.tests.androidx.test.core)
testImplementation(libs.tests.google.truth)
testImplementation(libs.tests.junit)
testImplementation(libs.tests.mockk)
testImplementation(libs.tests.robolectric)
}
@@ -17,12 +17,78 @@
package com.itsaky.androidide.templates.impl.emptyActivity
import com.itsaky.androidide.templates.Language
import com.itsaky.androidide.templates.ProjectTemplate
import com.itsaky.androidide.templates.base.AndroidModuleTemplateBuilder
import com.itsaky.androidide.templates.base.baseProject
import com.itsaky.androidide.templates.base.modules.android.defaultAppModule
fun emptyActivityProject(): ProjectTemplate = baseProject {
defaultAppModule {
recipe = {
writeEmptyActivityModule()
}
}
}
}
internal fun AndroidModuleTemplateBuilder.writeEmptyActivityModule() {
sources {
if (data.language == Language.Kotlin) {
writeKtSrc(data.packageName, "MainActivity",
source = this@writeEmptyActivityModule::emptyActivityKtSrc)
} else {
writeJavaSrc(packageName = data.packageName, className = "MainActivity",
source = this@writeEmptyActivityModule::emptyActivityJavaSrc)
}
}
}
private fun AndroidModuleTemplateBuilder.emptyActivityKtSrc(): String {
return """
package ${data.packageName}
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import ${data.packageName}.databinding.ActivityMainBinding
public class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Inflate and get instance of binding
binding = ActivityMainBinding.inflate(layoutInflater)
// set content view to binding's root
setContentView(binding.root)
}
}
"""
}
private fun AndroidModuleTemplateBuilder.emptyActivityJavaSrc(): String {
return """
package ${data.packageName};
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import ${data.packageName}.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Inflate and get instance of binding
binding = ActivityMainBinding.inflate(getLayoutInflater());
// set content view to binding's root
setContentView(binding.getRoot());
}
}
"""
}
@@ -22,6 +22,7 @@ import com.itsaky.androidide.templates.base.modules.android.ManifestActivity
import com.itsaky.androidide.templates.base.modules.android.defaultAppModule
import com.itsaky.androidide.templates.base.modules.createConstructor
import com.itsaky.androidide.templates.base.modules.createMethod
import com.itsaky.androidide.templates.impl.emptyActivity.writeEmptyActivityModule
import com.itsaky.androidide.xml.permissions.Permission
import com.squareup.javapoet.ArrayTypeName
import com.squareup.javapoet.ParameterSpec
@@ -32,6 +33,7 @@ import jdkx.lang.model.element.Modifier.STATIC
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.lang.String
/**
* Test template builder.
@@ -99,7 +101,7 @@ class TemplateBuilderTest {
ManifestActivity(name = ".MainActivity", isExported = true, isLauncher = true))
}
java {
sources {
createClass("com.itsaky", "TestClass") {
createConstructor {
addModifiers(PRIVATE)
@@ -109,8 +111,7 @@ class TemplateBuilderTest {
addModifiers(PUBLIC, STATIC)
returns(TypeName.VOID)
addParameter(
ParameterSpec.builder(ArrayTypeName.get(java.lang.String::class.java), "args")
.build())
ParameterSpec.builder(ArrayTypeName.get(String::class.java), "args").build())
addStatement("System.out.println(\"Hello world!\")")
}
}
@@ -123,4 +124,23 @@ class TemplateBuilderTest {
template.setupRootProjectParams()
template.executeRecipe()
}
@Test
fun `test empty activity template`() {
val template = testTemplate {
defaultAppModule {
recipe = {
writeEmptyActivityModule()
}
}
}
// Write the Java version of the project
template.setupRootProjectParams("EmptyActivityProjectJava", language = Language.Java)
template.executeRecipe()
// Write the Kotlin version of the project
template.setupRootProjectParams("EmptyActivityProjectKt", language = Language.Kotlin)
template.executeRecipe()
}
}
@@ -17,6 +17,7 @@
package com.itsaky.androidide.templates
import com.itsaky.androidide.utils.FileProvider
import java.io.File
import java.io.InputStream
@@ -28,7 +29,7 @@ import java.io.InputStream
class TestRecipeExecutor : RecipeExecutor {
private val assets by lazy {
File("./src/main/assets")
File(FileProvider.projectRoot().toFile(), "templates-api/src/main/assets")
}
override fun copy(source: File, dest: File) {
@@ -51,6 +52,6 @@ class TestRecipeExecutor : RecipeExecutor {
}
override fun copyAssetsRecursively(path: String, destDir: File) {
File(this.assets, path).copyRecursively(destDir)
File(this.assets, path).copyRecursively(destDir, true)
}
}
@@ -21,8 +21,8 @@ import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.managers.PreferenceManager
import com.itsaky.androidide.preferences.internal.prefManager
import com.itsaky.androidide.templates.base.baseProject
import com.itsaky.androidide.templates.base.ProjectTemplateConfigurator
import com.itsaky.androidide.templates.base.baseProject
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.FileProvider
import io.mockk.every
@@ -62,16 +62,19 @@ fun testTemplate(block: ProjectTemplateConfigurator): Template {
}
}
fun Template.setupRootProjectParams() {
fun Template.setupRootProjectParams(name: String = "TestTemplate",
packageName: String = "com.itsaky.androidide.template",
language: Language = Language.Kotlin, minSdk: Sdk = Sdk.Lollipop
) {
val iterator = parameters.iterator()
// name
var param = iterator.next()
(param as StringParameter).value = "TestTemplate"
(param as StringParameter).value = name
// package
param = iterator.next()
(param as StringParameter).value = "com.itsaky.androidide.template"
(param as StringParameter).value = packageName
// save location
param = iterator.next()
@@ -79,11 +82,11 @@ fun Template.setupRootProjectParams() {
// language
param = iterator.next()
(param as EnumParameter<Language>).value = Language.Kotlin
(param as EnumParameter<Language>).value = language
// Min SDK
param = iterator.next()
(param as EnumParameter<Sdk>).value = Sdk.Lollipop
(param as EnumParameter<Sdk>).value = minSdk
}
fun Template.executeRecipe() {