All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.restate.sdk.kotlin.gen.ServiceProcessor.kt Maven / Gradle / Ivy

The newest version!
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.kotlin.gen

import com.github.jknack.handlebars.io.ClassPathTemplateLoader
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.containingFile
import com.google.devtools.ksp.getKotlinClassByName
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.Origin
import dev.restate.sdk.common.ServiceType
import dev.restate.sdk.common.syscalls.ServiceDefinitionFactory
import dev.restate.sdk.gen.model.Service
import dev.restate.sdk.gen.template.HandlebarsTemplateEngine
import java.io.BufferedWriter
import java.io.IOException
import java.io.Writer
import java.nio.charset.Charset

class ServiceProcessor(private val logger: KSPLogger, private val codeGenerator: CodeGenerator) :
    SymbolProcessor {

  companion object {
    private val RESERVED_METHOD_NAMES: Set = setOf("send", "submit", "workflowHandle")
  }

  private val bindableServiceFactoryCodegen: HandlebarsTemplateEngine =
      HandlebarsTemplateEngine(
          "ServiceDefinitionFactory",
          ClassPathTemplateLoader(),
          mapOf(
              ServiceType.SERVICE to "templates/ServiceDefinitionFactory",
              ServiceType.WORKFLOW to "templates/ServiceDefinitionFactory",
              ServiceType.VIRTUAL_OBJECT to "templates/ServiceDefinitionFactory"),
          RESERVED_METHOD_NAMES)
  private val clientCodegen: HandlebarsTemplateEngine =
      HandlebarsTemplateEngine(
          "Client",
          ClassPathTemplateLoader(),
          mapOf(
              ServiceType.SERVICE to "templates/Client",
              ServiceType.WORKFLOW to "templates/Client",
              ServiceType.VIRTUAL_OBJECT to "templates/Client"),
          RESERVED_METHOD_NAMES)
  private val definitionsCodegen: HandlebarsTemplateEngine =
      HandlebarsTemplateEngine(
          "Definitions",
          ClassPathTemplateLoader(),
          mapOf(
              ServiceType.SERVICE to "templates/Definitions",
              ServiceType.WORKFLOW to "templates/Definitions",
              ServiceType.VIRTUAL_OBJECT to "templates/Definitions"),
          RESERVED_METHOD_NAMES)

  @OptIn(KspExperimental::class)
  override fun process(resolver: Resolver): List {
    val converter =
        KElementConverter(
            logger,
            resolver.builtIns,
            resolver.getKotlinClassByName(ByteArray::class.qualifiedName!!)!!.asType(listOf()))

    val resolved =
        resolver
            .getSymbolsWithAnnotation(dev.restate.sdk.annotation.Service::class.qualifiedName!!)
            .toSet() +
            resolver
                .getSymbolsWithAnnotation(
                    dev.restate.sdk.annotation.VirtualObject::class.qualifiedName!!)
                .toSet() +
            resolver
                .getSymbolsWithAnnotation(
                    dev.restate.sdk.annotation.Workflow::class.qualifiedName!!)
                // Workflow annotation can be set on functions too
                .filter { ksAnnotated -> ksAnnotated is KSClassDeclaration }
                .toSet()

    val services =
        resolved
            .filter { it.containingFile!!.origin == Origin.KOTLIN }
            .map {
              val serviceBuilder = Service.builder()
              converter.visitAnnotated(it, serviceBuilder)

              var serviceModel: Service? = null
              try {
                serviceModel = serviceBuilder.validateAndBuild()
              } catch (e: Exception) {
                logger.error("Unable to build service: $e", it)
              }
              (it to serviceModel!!)
            }
            .toList()

    // Run code generation
    for (service in services) {
      try {
        val fileCreator: (String) -> Writer = { name: String ->
          codeGenerator
              .createNewFile(
                  Dependencies(false, service.first.containingFile!!),
                  service.second.targetPkg.toString(),
                  name)
              .writer(Charset.defaultCharset())
        }
        this.bindableServiceFactoryCodegen.generate(fileCreator, service.second)
        this.clientCodegen.generate(fileCreator, service.second)
        this.definitionsCodegen.generate(fileCreator, service.second)
      } catch (ex: Throwable) {
        throw RuntimeException(ex)
      }
    }

    // META-INF
    if (services.isNotEmpty()) {
      generateMetaINF(services)
    }

    return emptyList()
  }

  private fun generateMetaINF(services: List>) {
    val resourceFile = "META-INF/services/${ServiceDefinitionFactory::class.java.canonicalName}"
    val dependencies =
        Dependencies(true, *(services.map { it.first.containingFile!! }.toTypedArray()))

    val writer: BufferedWriter =
        try {
          codeGenerator.createNewFileByPath(dependencies, resourceFile, "").bufferedWriter()
        } catch (e: FileSystemException) {
          val existingFile = e.file
          val currentValues = existingFile.readText()
          val newWriter = e.file.bufferedWriter()
          newWriter.write(currentValues)
          newWriter
        }

    try {
      writer.use {
        for (service in services) {
          it.write("${service.second.generatedClassFqcnPrefix}ServiceDefinitionFactory")
          it.newLine()
        }
      }
    } catch (e: IOException) {
      logger.error("Unable to create $resourceFile: $e")
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy