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

camundala.api.ApiCreator.scala Maven / Gradle / Ivy

package camundala.api

import camundala.BuildInfo
import camundala.bpmn.InputParams
import camundala.domain.*
import com.typesafe.config.ConfigFactory
import io.circe.Encoder
import sttp.apispec.openapi.*
import sttp.apispec.openapi.circe.yaml.*
import sttp.tapir.*
import sttp.tapir.docs.apispec.DocsExtension
import sttp.tapir.docs.openapi.{OpenAPIDocsInterpreter, OpenAPIDocsOptions}

import java.text.SimpleDateFormat
import java.util.Date
import scala.util.matching.Regex
import scala.jdk.CollectionConverters.*

trait ApiCreator extends PostmanApiCreator, TapirApiCreator, App:

  protected def companyProjectVersion: String
  protected def projectDescr: String

  def supportedVariables: Seq[InputParams] =
    InputParams.values.toSeq

  def document(apis: CApi*): Unit =
    val apiDoc = ApiDoc(apis.toList)
    apiConfig.projectsConfig.init // pulls all dependencies.
    ModelerTemplGenerator(version, apiConfig.modelerTemplateConfig, Some(projectName)).generate(
      collectApis(apiDoc)
    )
    ModelerTemplUpdater(apiConfig).update()
    writeOpenApis(apiDoc)
    writeCatalog(apiDoc)
  end document

  private def writeOpenApis(apiDoc: ApiDoc): Unit =
    writeOpenApi(
      apiConfig.openApiPath,
      openApi(apiDoc),
      apiConfig.openApiDocuPath
    )
    writeOpenApi(
      apiConfig.postmanOpenApiPath,
      postmanOpenApi(apiDoc),
      apiConfig.postmanOpenApiDocuPath
    )
    println(s"Check Open API Docu: ${apiConfig.openApiDocuPath}")
  end writeOpenApis

  protected lazy val openAPIDocsInterpreter      =
    OpenAPIDocsInterpreter(docsOptions =
      OpenAPIDocsOptions.default.copy(defaultDecodeFailureOutput = _ => None)
    )
  import sttp.tapir.json.circe.*
  protected def openApi(apiDoc: ApiDoc): OpenAPI =
    val endpoints = create(apiDoc)
    openAPIDocsInterpreter
      .toOpenAPI(
        endpoints,
        info(title, Some(description)),
        docsExtensions = List(DocsExtension.of(
          "tags",
          apiDoc.groupTags.asJson
        ))
      )

  end openApi

  protected def postmanOpenApi(apiDoc: ApiDoc): OpenAPI =
    val endpoints = createPostman(apiDoc)
    openAPIDocsInterpreter
      .toOpenAPI(endpoints, info(title, Some(postmanDescription)))
      .servers(servers)
  end postmanOpenApi

  protected def createChangeLog(): String =
    val changeLogFile = basePath / "CHANGELOG.md"
    if changeLogFile.toIO.exists() then
      s"""
         |
|CHANGELOG.md |

| |${os.read .lines(changeLogFile) .tail .map(_.replace("##", "###")) .map(replaceJira(_, apiConfig.jiraUrls)) .mkString("\n")} | |

|
|""".stripMargin else "" end if end createChangeLog protected def createGeneralVariables(): String = s"""|

|

| |General Variables | | |

| |### Mocking |""".stripMargin + createGeneralVariable( InputParams.servicesMocked, "Mock all the _ServiceWorkers_ in your process with their default Mock:", "process(..)\n .mockServices", s"\"${InputParams.servicesMocked}\": true," ) + createGeneralVariable( InputParams.mockedWorkers, s"""Mock any Process- and/or ExternalTask-Worker with their default Mocks. |This is a list of the _Worker topicNames or Process processNames_, you want to mock. |${listOfStringsOrCommaSeparated("mySubProcess,myOtherSubProcess,myService")} | |_Be aware_: For Sub-Processes, this expects an _InitWorker_ where the _topicName_ is equal to the _processName_. |""".stripMargin, """process(..) | .mockedWorkers("mySubProcess1", "mySubProcess2") // creates a list with SubProcessess | .mockedWorker("myOtherSubProcess") // adds a SubProcess""".stripMargin, """"mockedWorkers": ["mySubProcess", "myOtherSubProcess, myService"],""" ) + createGeneralVariable( InputParams.outputMock, """Mock the Process or ExternalTask (`Out`) | - You find an example in every _Process_ and _ExternalTask_. |""".stripMargin, """process(..) // or serviceTask(..)/customTask(..) | .mockWith(outputMock)""".stripMargin, """"outputMock": {..},""" ) + createGeneralVariable( InputParams.outputServiceMock, """Mock the Inner-Service (`MockedServiceResponse[ServiceOut]`) | - You find an example in every _ServiceTask_. |""".stripMargin, """serviceTask(..) | .mockServiceWith(MockedServiceResponse | .success200(inOut.defaultServiceOutMock))""".stripMargin, s""""outputServiceMock": ${MockedServiceResponse .success200("Example String Body") .asJson.deepDropNullValues},""".stripMargin ) + "### Mapping" + createGeneralVariable( InputParams.outputVariables, s"""You can filter the Output with a list of variable names you are interested in. |This list may include all variables from the output (`Out`). We included an example for each Process or ExternalTask. |${listOfStringsOrCommaSeparated("name,firstName")} |""".stripMargin, """process(..) // or serviceTask(..)/customTask(..) | .withOutputVariables("name", "firstName") // creates a list with outputVariables | .withOutputVariable("nickname") // adds a outputVariable""".stripMargin, """"outputVariables": ["name", "firstName"],""" ) + createGeneralVariable( InputParams.manualOutMapping, s"""By default all output Variables (`Out`) are on the Process for _External Tasks_. |If the filter _${InputParams.outputVariables}_ is not enough, |you can set this variable - every output variable is then local. | |_Be aware_ that you must then manually have _output mappings_ for each output variable! |""".stripMargin, """serviceTask(..) // or customTask(..) | .manualOutMapping""".stripMargin, """"manualOutMapping": true,""" ) + "### Mocking" + createGeneralVariable( InputParams.handledErrors, s"""A list of error codes that are handled (`BpmnError`) |${listOfStringsOrCommaSeparated("validation-failed,404")} | |At the moment only _ServiceTasks_ supported. |""".stripMargin, """serviceTask(..) | .handleErrors(ErrorCodes.`validation-failed`, "404") // create a list of handledErrors | .handleError("404") // add a handledError""".stripMargin, s""""handledErrors": ["validation-failed", "404"],""".stripMargin ) + createGeneralVariable( InputParams.regexHandledErrors, s"""You can further filter Handled Errors with a list of Regex expressions that the body error message must match. |${listOfStringsOrCommaSeparated( "SQL exception,\"errorNr\":\"20000\"" )} | |At the moment only _ServiceTasks_ supported. |""".stripMargin, """serviceTask(..) | .handleErrorWithRegex("SQL exception") | .handleErrorWithRegex("\"errorNr\":\"20000\"")""".stripMargin, s""""regexHandledErrors": ["SQL exception", "\"errorNr\":\"20000\""],""".stripMargin ) + "### Authorization" + createGeneralVariable( InputParams.impersonateUserId, """User-ID of a User that should be taken to authenticate to the services. |This must be supported by your implementation. *Be caution: this may be a security issue!*. |It is helpful if you have Tokens that expire, but long running Processes.""".stripMargin, """process(..) // or serviceTask(..)/customTask(..) | .withImpersonateUserId(impersonateUserId)""".stripMargin, """"impersonateUserId": "myUserName",""" ) + """

|
|

""".stripMargin end createGeneralVariables def createGeneralVariable( key: InputParams, descr: String, scalaExample: String, jsonExample: String ) = if supportedVariables.contains(key) then s""" |**$key**: | |$descr | |- DSL: |```scala |$scalaExample |``` | |- Json |```json |... |$jsonExample |... |``` | |""".stripMargin else "" end createGeneralVariable protected def replaceJira( line: String, jiraUrls: Map[String, String] ): String = jiraUrls.toList match case Nil => line case (k -> url) :: tail => val regex = Regex(s"""$k-(\\d+)""") val matches = regex.findAllIn(line).toSeq val changed = matches.foldLeft(line)((a, b) => a.replace(b, s"[$b]($url/$b)")) replaceJira(changed, tail.toMap) protected def packageConf = if packageConfPath.toIO.exists() then s"""# Package Configuration |**Check all dependency trees here: [$projectName](../../dependencies/$projectName.html)** | |$dependencies | |

|${apiConfig.projectsConfig.projectConfPath} |

| |``` | |${os.read(packageConfPath).trim} | |``` | |

|
|""".stripMargin else "" protected def dependencies: String = def docPortal(pckg: String) = apiConfig.docProjectUrl(pckg) / "OpenApi.html" val projects = apiConfig.projectsConfig.groupedConfigs.flatMap(_.projects) println(s"Projects: $projects") def documentations = projects.map(pc => pc.name -> docPortal(pc.name)).toMap s"""|### Dependencies: | |${ apiProjectConf.dependencies .map(dep => s"- _**[${dep.name}](${documentations.getOrElse(dep.name, "NOT FOUND")})**_") .mkString("\n") } |""".stripMargin end dependencies protected def createReadme(): String = val readme = basePath / "README.md" if readme.toIO.exists() then os.read.lines(readme).tail.mkString("\n") else "There is no README.md in the Project." end createReadme protected def description: String = s""" | |$projectDescr | |Created at ${SimpleDateFormat().format(new Date())} | |**See the [Camundala Documentation](https://pme123.github.io/camundala/)** | |$packageConf | |${createReadme()} | |${createChangeLog()} | |${createGeneralVariables()} | |> Created with: |> - [camundala-api v${BuildInfo.version}](https://github.com/pme123/camundala) |> - ${apiProjectConf.org}-camundala-api $companyProjectVersion |> - ${apiProjectConf.org}-camundala-helper | |""".stripMargin protected def postmanDescription: String = s""" |**This is for Postman - to have example requests. Be aware the Output is not provided!** | |$description |""" private def writeOpenApi( path: os.Path, api: OpenAPI, docPath: os.Path ): Unit = if os.exists(path) then os.remove(path) val yaml = api.toYaml os.write(path, yaml) println(s"Created Open API $path") println(s"See Open API Html $docPath") end writeOpenApi private def writeCatalog(apiDoc: ApiDoc): Unit = val catalogPath = apiConfig.catalogPath if os.exists(catalogPath) then os.remove(catalogPath) val catalog = toCatalog(apiDoc) os.write(catalogPath, catalog) println(s"Created Catalog $catalogPath") end writeCatalog private def toCatalog(apiDoc: ApiDoc): String = val optimizedApis = collectApisWithGroup(apiDoc) s"""### $title |${toCatalog(optimizedApis)} |""".stripMargin end toCatalog // takes exactly one api private def collectApisWithGroup(apiDoc: ApiDoc): List[(InOutApi[?, ?], String)] = apiDoc.apis.foldLeft(List.empty[(InOutApi[?, ?], String)]) { case (result, groupedApi: ProcessApi[?, ?, ?]) => result ++ (groupedApi.apis :+ groupedApi).map(_ -> groupedApi.name) case (result, groupedApi: GroupedApi) => result ++ groupedApi.apis.map(_ -> groupedApi.name) case (result, _) => result }.distinct // takes exactly one api private def collectApis(apiDoc: ApiDoc): List[InOutApi[?, ?]] = collectApisWithGroup(apiDoc) .map: _._1 private def toCatalog( apis: List[(InOutApi[?, ?], String)] ): String = apis .map: case api -> anchor => s"- ${createLink(api.endpointName, Some(anchor))}" .sorted .mkString("\n") end toCatalog private def listOfStringsOrCommaSeparated(example: String) = s"""It is also possible to use a _comma separated_ String, |like `"$example"`""".stripMargin private lazy val packageConfPath = apiConfig.basePath / apiConfig.projectsConfig.projectConfPath private lazy val apiProjectConf = ApiProjectConf(packageConfPath) end ApiCreator




© 2015 - 2025 Weber Informatics LLC | Privacy Policy