diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12734624c2c7..aa1bb3883218 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,5 @@ # This is the module where the Android Design System resides # Any changes to the source files of this module require approval from @malmstein or @nalcalag. /common/common-ui/ @malmstein @nalcalag -/saved-sites/ @malmstein \ No newline at end of file +/saved-sites/ @malmstein +/example-feature/ @cdrussell \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 62eea9a006e7..b9d8d9122494 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -486,3 +486,7 @@ tasks.register('releaseTestCloud', Exec) { commandLine 'maestro', 'cloud', '--include-tags', 'releaseTest', "build/outputs/apk/play/release/duckduckgo-${buildVersionName()}-play-release.apk", '../.maestro' dependsOn 'assemblePlayRelease' } + +task newModule(type: com.duckduckgo.gradle.ModuleCreator) { + feature = project.hasProperty('feature') ? project.getProperty('feature') : null +} \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000000..362bb8e29f99 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version "1.9.20" +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} diff --git a/buildSrc/src/main/java/com/duckduckgo/gradle/BuildGradleModifier.kt b/buildSrc/src/main/java/com/duckduckgo/gradle/BuildGradleModifier.kt new file mode 100644 index 000000000000..54209fffa515 --- /dev/null +++ b/buildSrc/src/main/java/com/duckduckgo/gradle/BuildGradleModifier.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.gradle + +import org.gradle.api.GradleException +import java.io.File + +class BuildGradleModifier(private val gradleFile: File) { + + fun removeDependency(placeholder: String) { + val lines = gradleFile.readText().lines() + val updatedLines = mutableListOf() + + for (line in lines) { + if (!line.contains(placeholder)) { + updatedLines.add(line) + } + } + + val newFileOutput = updatedLines.joinToString(System.lineSeparator()) + gradleFile.writeText(newFileOutput) + } + + fun insertDependencies( + featureName: String, + dependencies: List + ) { + if(dependencies.isEmpty()) return + + val searchString = "dependencies {" + val linesToAdd = dependencies.map { """implementation project(":${featureName}-${it}")""" } + + val lines = gradleFile.readText().lines() + val updatedLines = mutableListOf() + var found = false + + for (line in lines) { + updatedLines.add(line) + + if (line.contains(searchString)) { + linesToAdd.forEach { + if (!lines.contains("\t$it")) { + println("Inserting [$it] into [${gradleFile.parentFile.name}/${gradleFile.name}]") + updatedLines.add("\t$it") + } + } + found = true + } + } + + if (!found) { + throw GradleException("Could not insert dependencies into build.gradle because could not locate correct place to insert them.") + } + + val newFileOutput = updatedLines.joinToString(System.lineSeparator()) + gradleFile.writeText(newFileOutput) + } + + fun renameModuleNamespace( + featureName: String, + moduleType: String, + ) { + val lines = gradleFile.readText().lines() + val updatedLines = mutableListOf() + var found = false + val placeholder = moduleNamespacePlaceholder(moduleType) + val newNamespace = buildNewNamespace(featureName, moduleType) + + for (line in lines) { + if (line.contains(placeholder)) { + updatedLines.add(newNamespace) + found = true + } else { + updatedLines.add(line) + } + } + + if (!found) { + throw GradleException("Could not update namespace for [$featureName/$moduleType] because namespace placeholder not found in new module's build.gradle") + } + + val newFileOutput = updatedLines.joinToString(System.lineSeparator()) + gradleFile.writeText(newFileOutput) + } + + private fun buildNewNamespace( + featureName: String, + moduleType: String + ): String { + return """ namespace "com.duckduckgo.$featureName.${moduleType}"""" + } + + private fun moduleNamespacePlaceholder(moduleType: String): String { + return """namespace "com.duckduckgo.examplefeature.${moduleType}"""" + } +} diff --git a/buildSrc/src/main/java/com/duckduckgo/gradle/InputExtractor.kt b/buildSrc/src/main/java/com/duckduckgo/gradle/InputExtractor.kt new file mode 100644 index 000000000000..20234cb39256 --- /dev/null +++ b/buildSrc/src/main/java/com/duckduckgo/gradle/InputExtractor.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.gradle + +import org.gradle.api.GradleException + +class InputExtractor { + + fun extractFeatureNameAndTypes(input: String): Pair { + validateFeatureName(input) + val (featureName, moduleType) = input.split("/") + validateModuleType(moduleType) + return Pair(featureName, ModuleType.moduleTypeFromInput(moduleType)).also { + println("""Feature is [$featureName] and module type is [${moduleType}]""") + } + } + + private fun validateFeatureName(featureName: String) { + if (featureName.isEmpty()) throw GradleException(ERROR_MESSAGE_EMPTY_NAME) + if (!featureName.matches(ACCEPTABLE_CHARACTERS_REGEX)) throw GradleException(ERROR_MESSAGE_INVALID_CHARS) + if (featureName.count { it == '/' } != 1) throw GradleException(ERROR_MESSAGE_UNEXPECTED_NUMBER_OF_FORWARD_SLASHES) + if (featureName.startsWith("/")) throw GradleException(ERROR_MESSAGE_STARTS_WITH_FORWARD_SLASH) + } + + private fun validateModuleType(moduleType: String) { + if (!moduleType.matches(VALID_MODULE_TYPES_REGEX)) { + throw GradleException("Invalid module type [$moduleType]. Must be one of [ ${ModuleType.validInputTypes().joinToString(" | ")} ]") + } + } + + companion object { + private val VALID_MODULE_TYPES_REGEX = "^(${ModuleType.validInputTypes().joinToString("|")})$".toRegex() + private val ACCEPTABLE_CHARACTERS_REGEX = "^[a-z0-9-/]*$".toRegex() + + private const val ERROR_MESSAGE_EMPTY_NAME = "Feature name cannot be empty" + private const val ERROR_MESSAGE_UNEXPECTED_NUMBER_OF_FORWARD_SLASHES = "Feature name must contain exactly one forward slash" + private const val ERROR_MESSAGE_STARTS_WITH_FORWARD_SLASH = "Feature name cannot start with a forward slash" + private const val ERROR_MESSAGE_INVALID_CHARS = + "Feature name can only contain lowercase letters, numbers, dashes and one forward slash" + } +} diff --git a/buildSrc/src/main/java/com/duckduckgo/gradle/IntermoduleDependencyManager.kt b/buildSrc/src/main/java/com/duckduckgo/gradle/IntermoduleDependencyManager.kt new file mode 100644 index 000000000000..3573a708d076 --- /dev/null +++ b/buildSrc/src/main/java/com/duckduckgo/gradle/IntermoduleDependencyManager.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.gradle + +import com.duckduckgo.gradle.ModuleType.ApiPureKotlin +import com.duckduckgo.gradle.ModuleType.Companion.destinationDirectorySuffix +import com.duckduckgo.gradle.ModuleType.Impl +import java.io.File + +class IntermoduleDependencyManager { + + fun wireUpIntermoduleDependencies(newFeatureDestination: File) { + val apiModule = File(newFeatureDestination, "${newFeatureDestination.name}-${ApiPureKotlin.destinationDirectorySuffix()}") + val implModule = File(newFeatureDestination, "${newFeatureDestination.name}-${Impl.destinationDirectorySuffix()}") + + wireUpImplModule(apiModule, implModule, newFeatureDestination) + } + + private fun wireUpImplModule( + apiModule: File, + implModule: File, + newFeatureDestination: File + ) { + if (implModule.exists()) { + println("Wiring up module: ${implModule.name}") + val gradleModifier = BuildGradleModifier(File(implModule, BUILD_GRADLE)) + + // delete placeholder + gradleModifier.removeDependency(PLACEHOLDER_API_DEPENDENCY) + + // conditionally insert dependencies + val modules = mutableListOf() + if (apiModule.exists()) { + modules.add(ApiPureKotlin.destinationDirectorySuffix()) + } + gradleModifier.insertDependencies(newFeatureDestination.name, modules) + } + } + + fun wireUpAppModule( + featureName: String, + moduleType: ModuleType, + buildGradleFile: File, + ) { + println("Wiring up app module to include feature: name=[$featureName], type=[${moduleType.javaClass.simpleName}]") + val gradleModifier = BuildGradleModifier(buildGradleFile) + gradleModifier.insertDependencies(featureName, listOf(moduleType.destinationDirectorySuffix())) + } + + companion object { + private const val PLACEHOLDER_API_DEPENDENCY = "implementation project(':example-feature-api')" + private const val BUILD_GRADLE = "build.gradle" + } +} diff --git a/buildSrc/src/main/java/com/duckduckgo/gradle/ModuleCreator.kt b/buildSrc/src/main/java/com/duckduckgo/gradle/ModuleCreator.kt new file mode 100644 index 000000000000..f37382592e9d --- /dev/null +++ b/buildSrc/src/main/java/com/duckduckgo/gradle/ModuleCreator.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.gradle + +import com.duckduckgo.gradle.ModuleType.ApiPureKotlin +import com.duckduckgo.gradle.ModuleType.Companion.INPUT_API_ANDROID +import com.duckduckgo.gradle.ModuleType.Companion.INPUT_API_IMPL +import com.duckduckgo.gradle.ModuleType.Companion.INPUT_API_KOTLIN +import com.duckduckgo.gradle.ModuleType.Companion.destinationDirectorySuffix +import com.duckduckgo.gradle.ModuleType.Companion.exampleSubdirectorySuffix +import com.duckduckgo.gradle.ModuleType.Companion.namespaceSuffix +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class ModuleCreator : DefaultTask() { + + @get:Optional + @get:Input + abstract val feature: Property + + @TaskAction + fun performAction() { + val featureName = feature.orNull?.trim() ?: throw GradleException(ERROR_MESSAGE_MISSING_FEATURE.trim()) + println("Running module creation task, with input: [$featureName]") + + val (feature, moduleType) = extractFeatureAndModuleType(featureName) + + val newFeatureDestination = File(project.rootDir, feature) + val newModuleDestination = File(newFeatureDestination, "${feature}-${moduleType.destinationDirectorySuffix()}") + + newModuleDestination.ensureModuleDoesNotExist() + newModuleDestination.createDirectory() + newModuleDestination.copyTemplateFiles(getExampleSubDirectory(moduleType)) + newModuleDestination.renameModuleNamespace(feature, moduleType) + + copyTopLevelExampleFiles(newFeatureDestination) + + with(IntermoduleDependencyManager()) { + wireUpIntermoduleDependencies(newFeatureDestination) + wireUpAppModule(feature, moduleType, File(project.projectDir, ModuleCreator.BUILD_GRADLE)) + } + } + + private fun extractFeatureAndModuleType(featureName: String): Pair { + if (!featureName.contains("/")) { + throw GradleException(ERROR_MESSAGE_MISSING_FEATURE) + } + + val inputExtractor = InputExtractor() + return inputExtractor.extractFeatureNameAndTypes(featureName) + } + + private fun copyTopLevelExampleFiles(newFeatureDestination: File) { + getExampleDir().listFiles()?.filter { it.isFile }?.forEach { + if (!File(newFeatureDestination, it.name).exists()) { + it.copyTo(File(newFeatureDestination, it.name)) + } + } + } + + private fun File.ensureModuleDoesNotExist() { + if (exists()) throw GradleException("Feature [${relativeTo(project.rootDir)}] already exists") + } + + private fun File.createDirectory() { + if (!mkdirs()) throw GradleException("Failed to create directory at $path") + println("Created new directory at $path") + } + + private fun File.copyTemplateFiles(exampleDirectory: File) { + println("Using example files from ${exampleDirectory.path}") + + exampleDirectory.listFiles() + ?.filterNot { it.name == "build" } + ?.forEach { + val newFile = File(this, it.name) + it.copyRecursively(newFile) + } + } + + private fun getExampleDir(): File = File(project.rootDir, EXAMPLE_FEATURE_NAME) + + private fun getExampleSubDirectory(type: ModuleType): File { + val exampleDir = getExampleDir() + val subDirectory = "$EXAMPLE_FEATURE_NAME-${type.exampleSubdirectorySuffix()}" + val exampleDirectory = File(exampleDir, subDirectory) + if (!exampleDirectory.exists()) throw GradleException("Invalid module type [$type]. ${exampleDirectory.path} does not exist") + return exampleDirectory + } + + private fun File.renameModuleNamespace( + featureName: String, + moduleType: ModuleType + ) { + + // Pure kotlin modules don't need a namespace + if (moduleType == ApiPureKotlin) return + + val gradleModifier = BuildGradleModifier(File(this, BUILD_GRADLE)) + val formattedFeature = featureName.replace("-", "") + gradleModifier.renameModuleNamespace(formattedFeature, moduleType.namespaceSuffix()) + } + + companion object { + + private const val EXAMPLE_FEATURE_NAME = "example-feature" + private const val BUILD_GRADLE = "build.gradle" + + private const val ERROR_MESSAGE_MISSING_FEATURE = + "Feature name and module type not provided correctly. These must be provided as a command line argument in the format `-Pfeature=FEATURE/TYPE`" + + "\n" + + "\nTo create a pure Kotlin API module (preferred API type):" + + "\n\t./gradlew newModule -Pfeature=my-new-feature/${INPUT_API_KOTLIN}" + + "\n" + + "\nTo create an Android-aware API module:" + + "\n\t./gradlew newModule -Pfeature=my-new-feature/${INPUT_API_ANDROID}" + + "\n" + + "\nTo create an impl module:" + + "\n\t./gradlew newModule -Pfeature=my-new-feature/${INPUT_API_IMPL}" + } +} + + + diff --git a/buildSrc/src/main/java/com/duckduckgo/gradle/ModuleType.kt b/buildSrc/src/main/java/com/duckduckgo/gradle/ModuleType.kt new file mode 100644 index 000000000000..6fec2e8a8058 --- /dev/null +++ b/buildSrc/src/main/java/com/duckduckgo/gradle/ModuleType.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.gradle + +sealed class ModuleType { + + data object ApiPureKotlin : ModuleType() + data object ApiAndroid : ModuleType() + data object Impl : ModuleType() + + companion object { + + const val INPUT_API_KOTLIN = "api" + const val INPUT_API_ANDROID = "apiandroid" + const val INPUT_API_IMPL = "impl" + + fun moduleTypeFromInput(input: String): ModuleType { + return when (input) { + INPUT_API_KOTLIN -> ApiPureKotlin + INPUT_API_ANDROID -> ApiAndroid + INPUT_API_IMPL -> Impl + else -> throw IllegalArgumentException("Invalid module type [$input]") + } + } + + fun ModuleType.namespaceSuffix(): String { + return when (this) { + ApiAndroid -> "api" + Impl -> "impl" + else -> throw IllegalArgumentException("Module type [${javaClass.simpleName} does not have a module namespace suffix") + } + } + + fun ModuleType.destinationDirectorySuffix(): String { + return when (this) { + ApiAndroid -> "api" + ApiPureKotlin -> "api" + Impl -> "impl" + } + } + + fun ModuleType.exampleSubdirectorySuffix(): String { + return when (this) { + ApiAndroid -> "api-android" + ApiPureKotlin -> "api" + Impl -> "impl" + } + } + + fun validInputTypes(): List { + return listOf( + INPUT_API_KOTLIN, + INPUT_API_ANDROID, + INPUT_API_IMPL, + ) + } + } +} diff --git a/buildSrc/src/test/java/com/duckduckgo/gradle/InputExtractorTest.kt b/buildSrc/src/test/java/com/duckduckgo/gradle/InputExtractorTest.kt new file mode 100644 index 000000000000..267c2bd8f374 --- /dev/null +++ b/buildSrc/src/test/java/com/duckduckgo/gradle/InputExtractorTest.kt @@ -0,0 +1,83 @@ +package com.duckduckgo.gradle + +import com.duckduckgo.gradle.ModuleType.ApiAndroid +import com.duckduckgo.gradle.ModuleType.ApiPureKotlin +import com.duckduckgo.gradle.ModuleType.Impl +import org.gradle.api.GradleException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class InputExtractorTest{ + + private val testee = InputExtractor() + + @Test + fun whenFeatureNameIsEmptyThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("") } + } + + @Test + fun whenFeatureNameHasInvalidCharsThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("UPPERCASE") } + assertThrows { testee.extractFeatureNameAndTypes("!@£") } + assertThrows { testee.extractFeatureNameAndTypes("spaces not allowed") } + } + + @Test + fun whenFeatureNameStartsWithForwardSlashThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("/feature") } + } + + @Test + fun whenFeatureNameHasMoreThanOneForwardSlashThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("feature/foo/bar") } + } + + @Test + fun whenFeatureNameMissingAForwardSlashThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("feature") } + } + + @Test + fun whenFeatureNameHasDashSeparatorsThenValidates() { + val result =testee.extractFeatureNameAndTypes("feature-name/api") + assertEquals("feature-name", result.first) + } + + @Test + fun whenFeatureNameEndsWithForwardSlashThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("feature/") } + } + + @Test + fun whenFeatureNameSpecifiesStoreModuleTypeThenExceptionThrown() { + assertThrows {testee.extractFeatureNameAndTypes("feature/store") } + } + + @Test + fun whenInputSpeciesApiTypeThenPureKotlinApiTypeExtracted() { + val result = testee.extractFeatureNameAndTypes("feature/api") + assertEquals("feature", result.first) + assertEquals(ApiPureKotlin, result.second) + } + + @Test + fun whenInputSpeciesAndroidApiTypeThenAndroidApiTypeExtracted() { + val result = testee.extractFeatureNameAndTypes("feature/apiandroid") + assertEquals("feature", result.first) + assertEquals(ApiAndroid, result.second) + } + + @Test + fun whenInputSpeciesImplTypeThenImplTypeExtracted() { + val result = testee.extractFeatureNameAndTypes("feature/impl") + assertEquals("feature", result.first) + assertEquals(Impl, result.second) + } + + @Test + fun whenInputDoesNotSpecifyTypeTrailingForwardSlashThenExceptionThrown() { + assertThrows { testee.extractFeatureNameAndTypes("feature/") } + } +} diff --git a/buildSrc/src/test/java/com/duckduckgo/gradle/ModuleTypeTest.kt b/buildSrc/src/test/java/com/duckduckgo/gradle/ModuleTypeTest.kt new file mode 100644 index 000000000000..672a83dec51f --- /dev/null +++ b/buildSrc/src/test/java/com/duckduckgo/gradle/ModuleTypeTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("KotlinConstantConditions") + +package com.duckduckgo.gradle + +import com.duckduckgo.gradle.ModuleType.ApiAndroid +import com.duckduckgo.gradle.ModuleType.ApiPureKotlin +import com.duckduckgo.gradle.ModuleType.Companion +import com.duckduckgo.gradle.ModuleType.Companion.INPUT_API_KOTLIN +import com.duckduckgo.gradle.ModuleType.Companion.destinationDirectorySuffix +import com.duckduckgo.gradle.ModuleType.Companion.exampleSubdirectorySuffix +import com.duckduckgo.gradle.ModuleType.Companion.namespaceSuffix +import com.duckduckgo.gradle.ModuleType.Impl +import org.gradle.internal.impldep.junit.framework.TestCase.assertEquals +import org.gradle.internal.impldep.junit.framework.TestCase.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.lang.IllegalArgumentException + +class ModuleTypeTest { + + @Test + fun whenInputIsApiThenTypeIsKotlinApi() { + assertTrue(ModuleType.moduleTypeFromInput("api") is ApiPureKotlin) + } + + @Test + fun whenInputIsApiAndroidThenTypeIsAndroidApi() { + assertTrue(ModuleType.moduleTypeFromInput("apiandroid") is ApiAndroid) + } + + @Test + fun whenInputIsImplThenTypeIsImpl() { + assertTrue(ModuleType.moduleTypeFromInput("impl") is Impl) + } + + @Test + fun whenInputIsStoreThenExceptionIsThrown() { + assertThrows { ModuleType.moduleTypeFromInput("store") } + } + + @Test + fun whenInputIsUnknownThenExceptionIsThrown() { + assertThrows { ModuleType.moduleTypeFromInput("unknown") } + } + + @Test + fun whenKotlinApiThenNamespaceSuffixThrowsException() { + assertThrows { ApiPureKotlin.namespaceSuffix() } + } + + @Test + fun whenAndroidApiThenNamespaceSuffixIsApi() { + assertEquals("api", ApiAndroid.namespaceSuffix()) + } + + @Test + fun whenImplThenNamespaceSuffixIsImpl() { + assertEquals("impl", Impl.namespaceSuffix()) + } + + @Test + fun whenKotlinApiThenDestinationDirectorySuffixIsApi() { + assertEquals("api", ApiPureKotlin.destinationDirectorySuffix()) + } + + @Test + fun whenAndroidApiThenDestinationDirectorySuffixIsApi() { + assertEquals("api", ApiAndroid.destinationDirectorySuffix()) + } + + @Test + fun whenImplThenDestinationDirectorySuffixIsImpl() { + assertEquals("impl", Impl.destinationDirectorySuffix()) + } + + @Test + fun whenKotlinApiThenExampleDirectorySuffixIsApi() { + assertEquals("api", ApiPureKotlin.exampleSubdirectorySuffix()) + } + + @Test + fun whenAndroidApiThenExampleDirectorySuffixIsApi() { + assertEquals("api-android", ApiAndroid.exampleSubdirectorySuffix()) + } + + @Test + fun whenImplThenExampleDirectorySuffixIsImpl() { + assertEquals("impl", Impl.exampleSubdirectorySuffix()) + } +} diff --git a/example-feature/example-feature-api-android/.gitignore b/example-feature/example-feature-api-android/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/example-feature/example-feature-api-android/build.gradle b/example-feature/example-feature-api-android/build.gradle new file mode 100644 index 000000000000..de26ac0707f8 --- /dev/null +++ b/example-feature/example-feature-api-android/build.gradle @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +apply from: "$rootProject.projectDir/gradle/android-library.gradle" + +android { + namespace "com.duckduckgo.examplefeature.api" +} + +dependencies { + implementation project(':navigation-api') + + implementation KotlinX.coroutines.core + implementation AndroidX.appCompat +} + + diff --git a/example-feature/example-feature-api/.gitignore b/example-feature/example-feature-api/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/example-feature/example-feature-api/build.gradle b/example-feature/example-feature-api/build.gradle new file mode 100644 index 000000000000..247f455edd1f --- /dev/null +++ b/example-feature/example-feature-api/build.gradle @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'java-library' + id 'kotlin' +} + +apply from: "$rootProject.projectDir/code-formatting.gradle" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + implementation Kotlin.stdlib.jdk7 +} diff --git a/example-feature/example-feature-impl/build.gradle b/example-feature/example-feature-impl/build.gradle new file mode 100644 index 000000000000..44c651083711 --- /dev/null +++ b/example-feature/example-feature-impl/build.gradle @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'com.squareup.anvil' + id 'com.google.devtools.ksp' version "$ksp_version" +} + +apply from: "$rootProject.projectDir/gradle/android-library.gradle" + +dependencies { + implementation project(':example-feature-api') + + anvil project(path: ':anvil-compiler') + implementation project(path: ':anvil-annotations') + implementation project(path: ':di') + ksp AndroidX.room.compiler + + implementation KotlinX.coroutines.android + implementation AndroidX.core.ktx + implementation Google.dagger + + implementation "com.squareup.logcat:logcat:_" + + testImplementation Testing.junit4 + testImplementation "org.mockito.kotlin:mockito-kotlin:_" + testImplementation project(path: ':common-test') + testImplementation CashApp.turbine + testImplementation Testing.robolectric + testImplementation(KotlinX.coroutines.test) { + // https://github.com/Kotlin/kotlinx.coroutines/issues/2023 + // conflicts with mockito due to direct inclusion of byte buddy + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } +} + +android { + namespace "com.duckduckgo.examplefeature.impl" + anvil { + generateDaggerFactories = true // default is false + } + lint { + baseline file("lint-baseline.xml") + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + diff --git a/example-feature/example-feature-impl/lint-baseline.xml b/example-feature/example-feature-impl/lint-baseline.xml new file mode 100644 index 000000000000..f32fed49aac4 --- /dev/null +++ b/example-feature/example-feature-impl/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/example-feature/readme.md b/example-feature/readme.md new file mode 100644 index 000000000000..a720431c3db2 --- /dev/null +++ b/example-feature/readme.md @@ -0,0 +1,10 @@ +# Feature Name + +Describe the feature here. + +## Who can help you better understand this feature? +- ❓ +- ❓ + +## More information +- [Asana: feature documentation](❓) \ No newline at end of file