Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate code to have an API that provides module level metadata of Showkase elements #392

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import com.airbnb.android.showkase.processor.models.getShowkaseMetadataFromPrevi
import com.airbnb.android.showkase.processor.models.getShowkaseTypographyMetadata
import com.airbnb.android.showkase.processor.writer.PaparazziShowkaseScreenshotTestWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserProperties
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserPropertyWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseModuleBrowserPropertyWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter.Companion.CODEGEN_AUTOGEN_CLASS_NAME
import com.airbnb.android.showkase.processor.writer.ShowkaseCodegenMetadataWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseModuleCodegenMetadataWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseExtensionFunctionsWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseModuleApiWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseScreenshotTestWriter
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
Expand Down Expand Up @@ -262,16 +263,17 @@ class ShowkaseProcessor @JvmOverloads constructor(
val aggregateMetadataList = componentMetadata + colorMetadata + typographyMetadata
if (aggregateMetadataList.isEmpty()) return ShowkaseBrowserProperties()

ShowkaseCodegenMetadataWriter(environment).apply {
ShowkaseModuleCodegenMetadataWriter(environment).apply {
generateShowkaseCodegenFunctions(aggregateMetadataList)
}
ShowkaseBrowserPropertyWriter(environment).apply {
ShowkaseModuleBrowserPropertyWriter(environment).apply {
return generateMetadataPropertyFiles(
componentMetadata = componentMetadata,
colorMetadata = colorMetadata,
typographyMetadata = typographyMetadata,
)
}

}

private fun Collection<ShowkaseMetadata.Component>.dedupeAndSort() = this.distinctBy {
Expand Down Expand Up @@ -354,6 +356,16 @@ class ShowkaseProcessor @JvmOverloads constructor(
val currentShowkaseBrowserProperties =
writeMetadataFile(componentMetadata, colorMetadata, typographyMetadata)

ShowkaseModuleApiWriter.generateModuleLevelShowkaseProvider(
environment = environment,
moduleShowkaseBrowserProperties = currentShowkaseBrowserProperties
)

ShowkaseModuleApiWriter.generateModuleMetadataPublicApi(
environment = environment,
moduleShowkaseBrowserProperties = currentShowkaseBrowserProperties
)

if (rootElement != null) {
// This is the module that should aggregate all the other metadata files and
// also use the showkaseMetadata set from the current round to write the final file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,80 +71,6 @@ internal class ShowkaseBrowserWriter(private val environment: XProcessingEnv) {
)
}

private fun initializeComponentCodeBlock(
withoutParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
withParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
): CodeBlock {
val componentListInitializerCodeBlock = if (withParameterPropertyNames.isNotEmpty()) {
SHOWKASE_BROWSER_COMPONENT_CLASS_NAME.mutableListInitializerCodeBlock()
} else {
SHOWKASE_BROWSER_COMPONENT_CLASS_NAME.listInitializerCodeBlock()
}

componentListInitializerCodeBlock.apply {
addLineBreak()
withoutParameterPropertyNames.forEachIndexed { index, metadata ->
add("%M,", MemberName(metadata.propertyPackage, metadata.propertyName))
addLineBreak()
}
doubleUnindent()
add(")")

if (withParameterPropertyNames.isNotEmpty()) {
add(".apply {")
addLineBreak()
withDoubleIndent {
withParameterPropertyNames.forEachIndexed { index, metadata ->
add(
"addAll(%M)",
MemberName(metadata.propertyPackage, metadata.propertyName)
)
if (index != withParameterPropertyNames.lastIndex) {
addLineBreak()
}
}
}
closeCurlyBraces()
}
}

return componentListInitializerCodeBlock.build()
}

private fun initializeColorCodeBlock(
colorsParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
): CodeBlock {
val colorListInitializerCodeBlock =
SHOWKASE_BROWSER_COLOR_CLASS_NAME.listInitializerCodeBlock()

return colorListInitializerCodeBlock.apply {
addLineBreak()
colorsParameterPropertyNames.forEachIndexed { index, metadata ->
add("%M,", MemberName(metadata.propertyPackage, metadata.propertyName))
addLineBreak()
}
doubleUnindent()
add(")")
}.build()
}

private fun initializeTypographyCodeBlock(
typographyParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
): CodeBlock {
val typographyListInitializerCodeBlock =
SHOWKASE_BROWSER_TYPOGRAPHY_CLASS_NAME.listInitializerCodeBlock()

return typographyListInitializerCodeBlock.apply {
addLineBreak()
typographyParameterPropertyNames.forEachIndexed { index, metadata ->
add("%M,", MemberName(metadata.propertyPackage, metadata.propertyName))
addLineBreak()
}
doubleUnindent()
add(")")
}.build()
}

private fun initializeShowkaseRootCodegenAnnotation(
numComponentsWithoutPreviewParameter: Int,
numComponentsWithPreviewParameter: Int,
Expand Down Expand Up @@ -203,9 +129,9 @@ internal class ShowkaseBrowserWriter(private val environment: XProcessingEnv) {

companion object {
internal const val CODEGEN_AUTOGEN_CLASS_NAME = "Codegen"
private const val COMPONENT_INTERFACE_METHOD_NAME = "getShowkaseComponents"
private const val COLOR_INTERFACE_METHOD_NAME = "getShowkaseColors"
private const val TYPOGRAPHY_INTERFACE_METHOD_NAME = "getShowkaseTypography"
internal const val COMPONENT_INTERFACE_METHOD_NAME = "getShowkaseComponents"
internal const val COLOR_INTERFACE_METHOD_NAME = "getShowkaseColors"
internal const val TYPOGRAPHY_INTERFACE_METHOD_NAME = "getShowkaseTypography"
internal const val SHOWKASE_MODELS_PACKAGE_NAME = "com.airbnb.android.showkase.models"
internal const val COMPONENT_PROPERTY_NAME = "componentList"
internal const val COLOR_PROPERTY_NAME = "colorList"
Expand All @@ -220,5 +146,79 @@ internal class ShowkaseBrowserWriter(private val environment: XProcessingEnv) {
ClassName(SHOWKASE_MODELS_PACKAGE_NAME, "ShowkaseBrowserTypography")
internal val SHOWKASE_PROVIDER_CLASS_NAME =
ClassName(SHOWKASE_MODELS_PACKAGE_NAME, "ShowkaseProvider")

internal fun initializeComponentCodeBlock(
withoutParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
withParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
): CodeBlock {
val componentListInitializerCodeBlock = if (withParameterPropertyNames.isNotEmpty()) {
SHOWKASE_BROWSER_COMPONENT_CLASS_NAME.mutableListInitializerCodeBlock()
} else {
SHOWKASE_BROWSER_COMPONENT_CLASS_NAME.listInitializerCodeBlock()
}

componentListInitializerCodeBlock.apply {
addLineBreak()
withoutParameterPropertyNames.forEachIndexed { index, metadata ->
add("%M,", MemberName(metadata.propertyPackage, metadata.propertyName))
addLineBreak()
}
doubleUnindent()
add(")")

if (withParameterPropertyNames.isNotEmpty()) {
add(".apply {")
addLineBreak()
withDoubleIndent {
withParameterPropertyNames.forEachIndexed { index, metadata ->
add(
"addAll(%M)",
MemberName(metadata.propertyPackage, metadata.propertyName)
)
if (index != withParameterPropertyNames.lastIndex) {
addLineBreak()
}
}
}
closeCurlyBraces()
}
}

return componentListInitializerCodeBlock.build()
}

internal fun initializeColorCodeBlock(
colorsParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
): CodeBlock {
val colorListInitializerCodeBlock =
SHOWKASE_BROWSER_COLOR_CLASS_NAME.listInitializerCodeBlock()

return colorListInitializerCodeBlock.apply {
addLineBreak()
colorsParameterPropertyNames.forEachIndexed { index, metadata ->
add("%M,", MemberName(metadata.propertyPackage, metadata.propertyName))
addLineBreak()
}
doubleUnindent()
add(")")
}.build()
}

internal fun initializeTypographyCodeBlock(
typographyParameterPropertyNames: List<ShowkaseGeneratedMetadata>,
): CodeBlock {
val typographyListInitializerCodeBlock =
SHOWKASE_BROWSER_TYPOGRAPHY_CLASS_NAME.listInitializerCodeBlock()

return typographyListInitializerCodeBlock.apply {
addLineBreak()
typographyParameterPropertyNames.forEachIndexed { index, metadata ->
add("%M,", MemberName(metadata.propertyPackage, metadata.propertyName))
addLineBreak()
}
doubleUnindent()
add(")")
}.build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ internal class ShowkaseExtensionFunctionsWriter(
companion object {
private const val SHOWKASE_ROOT_MODULE_KEY = "SHOWKASE_ROOT_MODULE"
private const val INTENT_FUNCTION_NAME = "getBrowserIntent"
private const val METADATA_FUNCTION_NAME = "getMetadata"
internal const val METADATA_FUNCTION_NAME = "getMetadata"
private const val SHOWKASE_EXTENSION_FUNCTIONS_NAME = "ShowkaseExtensionFunctions"
private const val SHOWKASE_METHODS_SUFFIX = "${SHOWKASE_EXTENSION_FUNCTIONS_NAME}Codegen"
private const val CONTEXT_PARAMETER_NAME = "context"
Expand All @@ -127,7 +127,7 @@ internal class ShowkaseExtensionFunctionsWriter(
ClassName(CONTEXT_PACKAGE_NAME, "Intent")
private val SHOWKASE_BROWSER_ACTIVITY_CLASS_NAME =
ClassName("com.airbnb.android.showkase.ui", "ShowkaseBrowserActivity")
private val SHOWKASE_ELEMENTS_METADATA_CLASS_NAME =
internal val SHOWKASE_ELEMENTS_METADATA_CLASS_NAME =
ClassName(SHOWKASE_MODELS_PACKAGE_NAME, "ShowkaseElementsMetadata")
internal val SHOWKASE_OBJECT_CLASS_NAME =
ClassName(SHOWKASE_MODELS_PACKAGE_NAME, "Showkase")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.airbnb.android.showkase.processor.writer

import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XProcessingEnv
import androidx.room.compiler.processing.addOriginatingElement
import androidx.room.compiler.processing.writeTo
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter.Companion.SHOWKASE_PROVIDER_CLASS_NAME
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter.Companion.initializeColorCodeBlock
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter.Companion.initializeComponentCodeBlock
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter.Companion.initializeTypographyCodeBlock
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.LIST
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy

object ShowkaseModuleApiWriter {
@Suppress("LongMethod", "LongParameterList")
internal fun generateModuleLevelShowkaseProvider(
environment: XProcessingEnv,
moduleShowkaseBrowserProperties: ShowkaseBrowserProperties,
) {
if (moduleShowkaseBrowserProperties.isEmpty()) return
val packageName = moduleShowkaseBrowserProperties.getPackageName()
Copy link
Collaborator Author

@vinaygaba vinaygaba Jun 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elihart One of the challenges is which package to generate this API in. In the existing metadata APIs, we generate them in the package of the ~@ShowkaseRoot` module. As this file is generated in every module, I'm currently generating it in the package of the first element from this list. I don't quite like that. A fixed package isn't an option either since the API is identical in each module. Do you have any alternate suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the challenges is which module to generate this API in

Do you mean, "which package"?

It's hard for me to reason through what the new API looks like without examples in the PR summary or in the code - could you add something to show what the current output looks like?

What's your current strategy for how the test source codegen would know where to find the components for that module?

I think you may need to use an annotation to indicate the package. This could either be a new annotation (like @ShowkaseModule) or possibly you could reuse @ShowkaseRoot with the new expectation that it can be used in multiple modules.

Another option that comes to mind is that you could find the parent package common to all components in the module being processed. For example; com.example.a.b.foo and com.example.a.c.foo are processed and you identify com.example.a as the common package to generate the code in. The downside to this is it makes the output location a bit unpredictable, and prone to changing unexpectedly, so I think you probably need to use a fixed annotation.

For Airbnb, we could avoid manually adding that annotation, and only add it at CI time in each ui module before running code gen (and a similar process to also add the necessary annotation in the test sources for the test codegen)

val showkaseComponentsListClassName =
"${MODULE_LEVEL_SHOWKASE_PROVIDER_CLASS_PREFIX}_${packageName.normalizePackageName()}"
val fileBuilder = getFileBuilder(packageName, showkaseComponentsListClassName)
val componentCodeBlock = initializeComponentCodeBlock(
moduleShowkaseBrowserProperties.componentsWithoutPreviewParameters,
moduleShowkaseBrowserProperties.componentsWithPreviewParameters
)
val colorCodeBlock = initializeColorCodeBlock(moduleShowkaseBrowserProperties.colors)
val typographyCodeBlock =
initializeTypographyCodeBlock(moduleShowkaseBrowserProperties.typography)
writeFile(
environment,
fileBuilder,
ShowkaseBrowserWriter.SHOWKASE_PROVIDER_CLASS_NAME,
showkaseComponentsListClassName,
moduleShowkaseBrowserProperties,
getShowkaseProviderInterfaceFunction(
methodName = ShowkaseBrowserWriter.COMPONENT_INTERFACE_METHOD_NAME,
returnType = LIST.parameterizedBy(ShowkaseBrowserWriter.SHOWKASE_BROWSER_COMPONENT_CLASS_NAME),
codeBlock = componentCodeBlock
),
getShowkaseProviderInterfaceFunction(
methodName = ShowkaseBrowserWriter.COLOR_INTERFACE_METHOD_NAME,
returnType = LIST.parameterizedBy(ShowkaseBrowserWriter.SHOWKASE_BROWSER_COLOR_CLASS_NAME),
codeBlock = colorCodeBlock
),
getShowkaseProviderInterfaceFunction(
methodName = ShowkaseBrowserWriter.TYPOGRAPHY_INTERFACE_METHOD_NAME,
returnType = LIST.parameterizedBy(ShowkaseBrowserWriter.SHOWKASE_BROWSER_TYPOGRAPHY_CLASS_NAME),
codeBlock = typographyCodeBlock
),
showkaseRootCodegenAnnotation = null
)
}

internal fun generateModuleMetadataPublicApi(
environment: XProcessingEnv,
moduleShowkaseBrowserProperties: ShowkaseBrowserProperties,
) {
if (moduleShowkaseBrowserProperties.isEmpty()) return
val packageName = moduleShowkaseBrowserProperties.getPackageName()
val normalizedPackageName = packageName.normalizePackageName()
val showkaseComponentsListClassName =
"${MODULE_LEVEL_SHOWKASE_PROVIDER_CLASS_PREFIX}ExtensionFunctions_${normalizedPackageName}"
val fileBuilder = getFileBuilder(packageName, showkaseComponentsListClassName)

fileBuilder
.addFileComment("This is an auto-generated file. Please do not edit/modify this file.")
.addFunction(
FunSpec.builder(MODULE_METADATA_FUNCTION_NAME).apply {
receiver(ShowkaseExtensionFunctionsWriter.SHOWKASE_OBJECT_CLASS_NAME)
returns(ShowkaseExtensionFunctionsWriter.SHOWKASE_ELEMENTS_METADATA_CLASS_NAME)
addKdoc(
"Helper function that gives you access to Showkase elements that are " +
"declared in a given module. This contains data about the composables, " +
"colors and typography that are meant to be rendered inside the Showkase " +
"browser. This is different from the " +
"Showkase.${ShowkaseExtensionFunctionsWriter.METADATA_FUNCTION_NAME}() " +
"function, which contains all the Showkase elements in a given " +
"ShowkaseRoot graph, whereas this function only contains metadata " +
"about the module it's generated in. Each module where Showkase is " +
"setup will have this function generated in it."
)
addCode(
CodeBlock.Builder()
.indent()
.addStatement(
"return (%T() as %T).metadata()",
ClassName(
packageName,
"${MODULE_LEVEL_SHOWKASE_PROVIDER_CLASS_PREFIX}_${normalizedPackageName}"
),
SHOWKASE_PROVIDER_CLASS_NAME
)
.unindent()
.build()
)
moduleShowkaseBrowserProperties.zip()
.forEach { addOriginatingElement(it.element) }
}
.build()
)
.build()
.writeTo(environment.filer, mode = XFiler.Mode.Aggregating)
}

private const val MODULE_METADATA_FUNCTION_NAME = "getModuleMetadata"
private const val MODULE_LEVEL_SHOWKASE_PROVIDER_CLASS_PREFIX = "ShowkaseModuleMetadata"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.asTypeName

class ShowkaseBrowserPropertyWriter(private val environment: XProcessingEnv) {
class ShowkaseModuleBrowserPropertyWriter(private val environment: XProcessingEnv) {
@Suppress("LongMethod")
internal fun generateMetadataPropertyFiles(
componentMetadata: Set<ShowkaseMetadata.Component>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.TypeSpec
import java.util.Locale

internal class ShowkaseCodegenMetadataWriter(private val environment: XProcessingEnv) {
internal class ShowkaseModuleCodegenMetadataWriter(private val environment: XProcessingEnv) {

internal fun generateShowkaseCodegenFunctions(
showkaseMetadataSet: Set<ShowkaseMetadata>,
) {
val moduleName = showkaseMetadataSet.first().packageName.replace(".", "_")
val generatedClassName = "ShowkaseMetadata_${moduleName.lowercase(Locale.getDefault())}"
val generatedClassName = "ShowkaseMetadata_${showkaseMetadataSet.getNormalizedPackageName()}"
val fileBuilder = FileSpec.builder(
CODEGEN_PACKAGE_NAME,
generatedClassName
Expand Down
Loading
Loading