Skip to content

Commit

Permalink
Merge pull request #81 from jansigi/feature/schema-class-generation
Browse files Browse the repository at this point in the history
Feature/schema class generation
  • Loading branch information
jansigi authored Apr 10, 2024
2 parents be45338 + 1e8b16f commit 9f10311
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.schwarz.crystalapi

@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class SchemaClass
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.schwarz.crystalapi.schema

interface CMType {
val path: String
}

class CMField<T : Any>(val name: String, override val path: String) : CMType

class CMList<T : Any>(val name: String, override val path: String) : CMType

class CMObject<out T : Schema>(val element: T, override val path: String) : CMType

class CMObjectList<out T : Schema>(val element: T, val name: String, override val path: String) : CMType
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.schwarz.crystalapi.schema

interface Schema
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package com.schwarz.crystalprocessor

import com.google.auto.service.AutoService
import com.schwarz.crystalapi.Entity
import com.schwarz.crystalapi.Field
import com.schwarz.crystalapi.GenerateAccessor
import com.schwarz.crystalapi.MapWrapper
import com.schwarz.crystalapi.Reduce
import com.schwarz.crystalapi.Reduces
import com.schwarz.crystalapi.SchemaClass
import com.schwarz.crystalapi.mapify.Mapper
import com.schwarz.crystalapi.query.Queries
import com.schwarz.crystalapi.query.Query
import com.schwarz.crystalprocessor.CoachBaseBinderProcessor.Companion.FRAMEWORK_DOCUMENTATION_FILENAME_OPTION_NAME
import com.schwarz.crystalprocessor.CoachBaseBinderProcessor.Companion.FRAMEWORK_DOCUMENTATION_PATH_OPTION_NAME
import com.schwarz.crystalprocessor.CoachBaseBinderProcessor.Companion.FRAMEWORK_SCHEMA_FILENAME_OPTION_NAME
Expand All @@ -11,17 +21,25 @@ import com.schwarz.crystalprocessor.generation.CodeGenerator
import com.schwarz.crystalprocessor.processing.Worker
import com.schwarz.crystalprocessor.processing.mapper.MapperWorker
import com.schwarz.crystalprocessor.processing.model.ModelWorker
import com.schwarz.crystalapi.*
import com.schwarz.crystalapi.mapify.Mapper
import com.schwarz.crystalapi.query.Queries
import com.schwarz.crystalapi.query.Query
import javax.annotation.processing.*
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedOptions
import javax.annotation.processing.SupportedSourceVersion
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement

@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor::class)
@SupportedOptions(KAPT_KOTLIN_GENERATED_OPTION_NAME, FRAMEWORK_USE_SUSPEND_OPTION_NAME, FRAMEWORK_DOCUMENTATION_PATH_OPTION_NAME, FRAMEWORK_DOCUMENTATION_FILENAME_OPTION_NAME, FRAMEWORK_SCHEMA_PATH_OPTION_NAME, FRAMEWORK_SCHEMA_FILENAME_OPTION_NAME)
@SupportedOptions(
KAPT_KOTLIN_GENERATED_OPTION_NAME,
FRAMEWORK_USE_SUSPEND_OPTION_NAME,
FRAMEWORK_DOCUMENTATION_PATH_OPTION_NAME,
FRAMEWORK_DOCUMENTATION_FILENAME_OPTION_NAME,
FRAMEWORK_SCHEMA_PATH_OPTION_NAME,
FRAMEWORK_SCHEMA_FILENAME_OPTION_NAME
)
class CoachBaseBinderProcessor : AbstractProcessor() {

private lateinit var mLogger: Logger
Expand Down Expand Up @@ -81,6 +99,17 @@ class CoachBaseBinderProcessor : AbstractProcessor() {
}

override fun getSupportedAnnotationTypes(): MutableSet<String> {
return setOf(Field::class.java.canonicalName, Entity::class.java.canonicalName, MapWrapper::class.java.canonicalName, Queries::class.java.canonicalName, Query::class.java.canonicalName, GenerateAccessor::class.java.canonicalName, Mapper::class.java.canonicalName, Reduces::class.java.canonicalName, Reduce::class.java.canonicalName).toMutableSet()
return setOf(
Field::class.java.canonicalName,
Entity::class.java.canonicalName,
MapWrapper::class.java.canonicalName,
SchemaClass::class.java.canonicalName,
Queries::class.java.canonicalName,
Query::class.java.canonicalName,
GenerateAccessor::class.java.canonicalName,
Mapper::class.java.canonicalName,
Reduces::class.java.canonicalName,
Reduce::class.java.canonicalName
).toMutableSet()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.schwarz.crystalprocessor.generation.model

import com.schwarz.crystalapi.schema.CMField
import com.schwarz.crystalapi.schema.CMList
import com.schwarz.crystalapi.schema.CMObject
import com.schwarz.crystalapi.schema.CMObjectList
import com.schwarz.crystalapi.schema.Schema
import com.schwarz.crystalprocessor.model.entity.SchemaClassHolder
import com.schwarz.crystalprocessor.model.field.CblBaseFieldHolder
import com.schwarz.crystalprocessor.model.field.CblFieldHolder
import com.schwarz.crystalprocessor.util.ConversionUtil
import com.schwarz.crystalprocessor.util.TypeUtil
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName

/**
* This class is responsible for generating the Schema classes.
*
* To generate a SchemaClass, add the following annotation to your Class:
* ```
* @SchemaClass
* ```
* All the fields will then be generated into a new file.
*/
class SchemaGeneration {
private val pathAttributeName = "path"
fun generateModel(holder: SchemaClassHolder, schemaClassPaths: List<String>): FileSpec {
val packageName = holder.sourcePackage
val schemaClassName = holder.entitySimpleName

val schemaClass: TypeSpec.Builder = buildSchemaClass(schemaClassName)

buildAndAddFieldProperties(holder, schemaClass, schemaClassPaths)

return FileSpec.builder(packageName, schemaClassName).addType(schemaClass.build()).build()
}

private fun buildSchemaClass(className: String): TypeSpec.Builder {
val pathParameter = ParameterSpec.builder(pathAttributeName, String::class).defaultValue("%S", "").build()

return TypeSpec.classBuilder(className)
.addModifiers(KModifier.OPEN)
.addSuperinterface(Schema::class)
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameter(pathParameter)
.build()
)
}

private fun buildAndAddFieldProperties(
holder: SchemaClassHolder,
schemaClass: TypeSpec.Builder,
schemaClassPaths: List<String>
) {
buildAndAddConstantFieldProperties(holder, schemaClass, schemaClassPaths)
buildAndAddNormalFieldProperties(holder, schemaClass, schemaClassPaths)
}

private fun buildAndAddConstantFieldProperties(
holder: SchemaClassHolder,
schemaClass: TypeSpec.Builder,
schemaClassPaths: List<String>
) {
holder.fieldConstants.forEach { (fieldName, fieldObject) ->
val defaultVariableName = "DEFAULT_${fieldObject.constantName}"

val constantProperty = PropertySpec.builder(
defaultVariableName,
fieldObject.fieldType
).initializer(
ConversionUtil.convertStringToDesiredFormat(
fieldObject.typeMirror,
fieldObject.constantValue
)
)

schemaClass.addProperty(constantProperty.build())

buildAndAddFieldProperty(
schemaClass,
fieldName,
fieldObject,
schemaClassPaths
)
}
}

private fun buildAndAddNormalFieldProperties(
holder: SchemaClassHolder,
schemaClass: TypeSpec.Builder,
schemaClassPaths: List<String>
) {
holder.fields.forEach { (fieldName, fieldObject) ->
buildAndAddFieldProperty(
schemaClass,
fieldName,
fieldObject,
schemaClassPaths
)
}
}

private fun buildAndAddFieldProperty(
schemaClass: TypeSpec.Builder,
fieldName: String,
fieldObject: CblBaseFieldHolder,
schemaClassPaths: List<String>
): TypeSpec.Builder = schemaClass.addProperty(
buildFieldProperty(fieldObject, fieldName, schemaClassPaths)
)

private fun buildFieldProperty(
fieldObject: CblBaseFieldHolder,
fieldName: String,
schemaClassPaths: List<String>
): PropertySpec {
val isObject = schemaClassPaths.contains(fieldObject.typeMirror.toString())

val outerType = getOuterPropertyType(fieldObject.isIterable, isObject)

val innerType: TypeName = getInnerPropertyType(fieldObject)

return PropertySpec.builder(
fieldName,
outerType.parameterizedBy(innerType)
).initializer(
createPropertyFormat(fieldName, innerType, fieldObject.isIterable, isObject),
outerType
).build()
}

private fun createPropertyFormat(
fieldName: String,
propertyType: TypeName,
isIterable: Boolean,
isObject: Boolean
): String {
val propertyAccessPath =
"if ($pathAttributeName.isBlank()) \"$fieldName\" else \"\$$pathAttributeName.$fieldName\""

return when {
isIterable && isObject -> buildObjectListFormat(propertyType, fieldName, propertyAccessPath)
isObject -> buildObjectFormat(propertyType, propertyAccessPath)
else -> buildSimpleFormat(fieldName)
}
}

private fun buildObjectListFormat(propertyType: TypeName, fieldName: String, propertyAccessPath: String): String =
"""%T(
$propertyType($propertyAccessPath),
"$fieldName",
$pathAttributeName,
)"""

private fun buildSimpleFormat(fieldName: String): String =
"""%T("$fieldName", $pathAttributeName)"""

private fun buildObjectFormat(propertyType: TypeName, propertyAccessPath: String): String =
"""%T(
$propertyType($propertyAccessPath),
$pathAttributeName,
)"""

private fun getOuterPropertyType(
isIterable: Boolean,
isObject: Boolean
) = when {
isIterable && isObject -> CMObjectList::class.asTypeName()
isIterable -> CMList::class.asTypeName()
isObject -> CMObject::class.asTypeName()
else -> CMField::class.asTypeName()
}

private fun getInnerPropertyType(field: CblBaseFieldHolder): TypeName {
val subEntity = (field as? CblFieldHolder)?.subEntitySimpleName

return TypeUtil.parseMetaType(field.typeMirror, false, subEntity)
}
}
Loading

0 comments on commit 9f10311

Please sign in to comment.