Skip to content

Commit

Permalink
Merge pull request #314 from commercetools/bruno
Browse files Browse the repository at this point in the history
add bruno module
  • Loading branch information
jenschude authored May 23, 2024
2 parents 44635ef + 9d4108f commit 973d2c8
Show file tree
Hide file tree
Showing 14 changed files with 812 additions and 2 deletions.
8 changes: 8 additions & 0 deletions languages/bruno/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

dependencies {
implementation project(':codegen-renderers')
implementation commons.lang3
implementation commons.text
implementation orgkotlin.reflect
implementation orgkotlin.stdlib
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package io.vrap.codegen.languages.bruno.model

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ObjectNode
import io.vrap.codegen.languages.extensions.*
import io.vrap.rmf.codegen.firstUpperCase
import io.vrap.rmf.codegen.io.TemplateFile
import io.vrap.rmf.codegen.rendering.FileProducer
import io.vrap.rmf.codegen.rendering.utils.keepAngleIndent
import io.vrap.rmf.codegen.types.VrapTypeProvider
import io.vrap.rmf.raml.model.modules.Api
import io.vrap.rmf.raml.model.resources.HttpMethod
import io.vrap.rmf.raml.model.resources.Method
import io.vrap.rmf.raml.model.resources.Resource
import io.vrap.rmf.raml.model.types.*
import io.vrap.rmf.raml.model.util.StringCaseFormat
import java.io.IOException

class BrunoActionRenderer(val api: Api, override val vrapTypeProvider: VrapTypeProvider) : EObjectExtensions, FileProducer {

val offset = 1 + allResourceMethods().count()

fun allResourceMethods(): List<Method> = api.allContainedResources.flatMap { it.methods }

override fun produceFiles(): List<TemplateFile> {
return updateActions(api)
}

fun updateActions(api: Api): List<TemplateFile> {
val updatableResources = api.allContainedResources.filter { it.getAnnotation("updateable") != null }

return updatableResources.flatMap { resourceUpdates(it) }
}

fun resourceUpdates(resource: Resource): List<TemplateFile> {
val updateMethod = resource.getUpdateMethod()
return updateMethod?.getActions()?.filterNot { objType -> objType.deprecated() }?.mapIndexed { index, objectType -> renderAction(resource, updateMethod, objectType, index) } ?: return emptyList()
}

private fun renderAction(resource: Resource, method: Method, type: ObjectType, index: Int): TemplateFile {
val url = BrunoUrl(method.resource(), method) { methodResource, name -> when (name) {
"ID" -> methodResource.resourcePathName.singularize() + "-id"
"key" -> methodResource.resourcePathName.singularize() + "-key"
else -> StringCaseFormat.LOWER_HYPHEN_CASE.apply(name)
}}
val actionBody = resource.actionExample(type)
val name = "${type.discriminatorValue.firstUpperCase()}${if (type.markDeprecated()) " (deprecated)" else ""}"
val content = BrunoRequestRenderer.renderRequest(name, method, url, actionBody, index + offset)

val relativePath = methodResourcePath(method) + "/Update actions/" + type.discriminatorValue.firstUpperCase() + ".bru"

return TemplateFile(
relativePath = relativePath,
content = content
)
}

private fun Resource.actionExample(type: ObjectType): String {
val example = getExample(type)
return """
|{
| "version": {{${this.resourcePathName.singularize()}-version}},
| "actions": [
| <<${if (example.isNullOrEmpty().not()) example else """
| |{
| | "action": "${type.discriminatorValue}"
| |}""".trimMargin()}>>
| ]
|}
""".trimMargin().keepAngleIndent()
}

private fun getExample(type: ObjectType): String? {
var example: String? = null
var instance: Instance? = null

if (type.getAnnotation("postman-example") != null) {
instance = type.getAnnotation("postman-example").value
} else if (type.examples.size > 0) {
instance = type.examples[0].value
}

if (instance != null) {
example = instance.toJson()
try {
val mapper = ObjectMapper()
val nodes = mapper.readTree(example) as ObjectNode
nodes.put("action", type.discriminatorValue)

example = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(nodes)
.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().map { s -> " $s" }
.joinToString("\n")
.trim { it <= ' ' }
} catch (e: IOException) {
}

}

return example
}

private fun ObjectType.markDeprecated() : Boolean {
val anno = this.getAnnotation("markDeprecated")
return (anno != null && (anno.value as BooleanInstance).value)
}

private fun Resource.getUpdateMethod(): Method? {
val byIdResource = this.resources.find { resource -> resource.relativeUri.template == "/{ID}" } ?: return null

return byIdResource.getMethod(HttpMethod.POST)
}

private fun Method.getActions(): List<ObjectType> {
val body = this.getBody("application/json") ?: return emptyList()

val actions = (body.type as ObjectType).getProperty("actions") ?: return emptyList()

val actionsType = actions.type as ArrayType
val updateActions = if (actionsType.items is UnionType) {
(actionsType.items as UnionType).oneOf[0].subTypes
} else {
actionsType.items.subTypes
}
val actionItems = updateActions.map { action -> action as ObjectType }.sortedBy { action -> action.discriminatorValue }
return actionItems
}


private fun methodResourcePath(method: Method): String {
var resourcePathes = resourcePathes(method.resource())

var directories = resourcePathes.map { it.displayName?.value ?: it.resourcePathName.firstUpperCase() }
return directories.joinToString("/")
}

private fun resourcePathes(resource: Resource): List<Resource> {
if (resource.parent is Resource) {
if (resource.resourcePathName == resource.parent.resourcePathName) {
return resourcePathes(resource.parent)
}
return resourcePathes(resource.parent).plus(resource)
}
return listOf(resource)
}



fun Instance.toJson(): String {
var example = ""
val mapper = ObjectMapper()

val module = SimpleModule()
module.addSerializer(ObjectInstance::class.java, ObjectInstanceSerializer())
module.addSerializer<Instance>(ArrayInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(IntegerInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(BooleanInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(StringInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(NumberInstance::class.java, InstanceSerializer())
mapper.registerModule(module)

if (this is StringInstance) {
example = this.value
} else if (this is ObjectInstance) {
try {
example = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(this)
} catch (e: JsonProcessingException) {
}

}

return example
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.vrap.codegen.languages.bruno.model

import io.vrap.rmf.codegen.types.LanguageBaseTypes
import io.vrap.rmf.codegen.types.VrapScalarType

object BrunoBaseTypes : LanguageBaseTypes(
anyType = nativePostmanType("any"),
objectType = nativePostmanType("object"),
integerType = nativePostmanType("number"),
longType = nativePostmanType("number"),
doubleType = nativePostmanType("number"),
stringType = nativePostmanType("string"),
booleanType = nativePostmanType("boolean"),
dateTimeType = nativePostmanType("string"),
dateOnlyType = nativePostmanType("string"),
timeOnlyType = nativePostmanType("string"),
file = nativePostmanType("Buffer")
)

fun nativePostmanType(typeName: String): VrapScalarType = VrapScalarType(typeName)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.vrap.codegen.languages.bruno.model

import com.hypertino.inflector.English
import io.vrap.rmf.codegen.rendering.utils.escapeAll
import io.vrap.rmf.raml.model.modules.Api
import io.vrap.rmf.raml.model.security.OAuth20Settings
import io.vrap.rmf.raml.model.types.StringInstance
import org.apache.commons.text.StringEscapeUtils
import org.eclipse.emf.ecore.EObject
import java.net.URI


fun String.escapeJson(): String {
return StringEscapeUtils.escapeJson(this)
}

fun OAuth20Settings.uri(): URI {
return URI.create(this.accessTokenUri)
}

fun Api.oAuth2(): OAuth20Settings {
return this.securitySchemes.stream()
.filter { securityScheme -> securityScheme.settings is OAuth20Settings }
.map { securityScheme -> securityScheme.settings as OAuth20Settings }
.findFirst().orElse(null)
}

fun String.singularize(): String {
return English.singular(this)
}

@Suppress("UNCHECKED_CAST")
fun <T> EObject.getParent(parentClass: Class<T>): T? {
if (this.eContainer() == null) {
return null
}
return if (parentClass.isInstance(this.eContainer())) {
this.eContainer() as T
} else this.eContainer().getParent(parentClass)
}

fun StringInstance.description(): String {
return this.value.escapeJson().escapeAll()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package io.vrap.codegen.languages.bruno.model

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import io.vrap.codegen.languages.extensions.*
import io.vrap.rmf.codegen.firstUpperCase
import io.vrap.rmf.codegen.io.TemplateFile
import io.vrap.rmf.codegen.rendering.FileProducer
import io.vrap.rmf.codegen.rendering.utils.keepAngleIndent
import io.vrap.rmf.codegen.types.VrapTypeProvider
import io.vrap.rmf.raml.model.modules.Api
import io.vrap.rmf.raml.model.resources.Method
import io.vrap.rmf.raml.model.resources.Resource
import io.vrap.rmf.raml.model.types.*
import io.vrap.rmf.raml.model.util.StringCaseFormat

class BrunoMethodRenderer(val api: Api, override val vrapTypeProvider: VrapTypeProvider) : EObjectExtensions, FileProducer {

val offset = 1

fun allResourceMethods(): List<Method> = api.allContainedResources.flatMap { it.methods }

override fun produceFiles(): List<TemplateFile> {
return methods()
}

private fun methods(): List<TemplateFile> {
return allResourceMethods().mapIndexed { index, method -> render(index, method) }
}

fun render(index: Int, type: Method): TemplateFile {

val content = renderStr(index, type).trimMargin().keepAngleIndent()

val relativePath = methodResourcePath(type) + "/" + type.toRequestName() + ".bru"

return TemplateFile(
relativePath = relativePath,
content = content
)
}

fun renderStr(index: Int, method: Method): String {
val url = BrunoUrl(method.resource(), method) { resource, name -> when (name) {
"ID" -> resource.resourcePathName.singularize() + "-id"
"key" -> resource.resourcePathName.singularize() + "-key"
else -> StringCaseFormat.LOWER_HYPHEN_CASE.apply(name)
}}
val name = method.displayName?.value ?: "${method.methodName} ${method.resource().toResourceName()}"
return BrunoRequestRenderer.renderRequest(name, method, url, method.getExample(), index + offset)
}

fun Method.getExample(): String? {
val s = this.bodies?.
getOrNull(0)?.
type?.
examples?.
getOrNull(0)?.
value
return s?.toJson()
}

private fun methodResourcePath(method: Method): String {
var resourcePathes = resourcePathes(method.resource())

var directories = resourcePathes.map { it.displayName?.value ?: it.resourcePathName.firstUpperCase() }
return directories.joinToString("/")
}

private fun resourcePathes(resource: Resource): List<Resource> {
if (resource.parent is Resource) {
if (resource.resourcePathName == resource.parent.resourcePathName) {
return resourcePathes(resource.parent)
}
return resourcePathes(resource.parent).plus(resource)
}
return listOf(resource)
}



fun Instance.toJson(): String {
var example = ""
val mapper = ObjectMapper()

val module = SimpleModule()
module.addSerializer(ObjectInstance::class.java, ObjectInstanceSerializer())
module.addSerializer<Instance>(ArrayInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(IntegerInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(BooleanInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(StringInstance::class.java, InstanceSerializer())
module.addSerializer<Instance>(NumberInstance::class.java, InstanceSerializer())
mapper.registerModule(module)

if (this is StringInstance) {
example = this.value
} else if (this is ObjectInstance) {
try {
example = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(this)
} catch (e: JsonProcessingException) {
}

}

return example
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.vrap.codegen.languages.bruno.model

import io.vrap.rmf.codegen.di.RamlGeneratorModule
import io.vrap.rmf.codegen.di.Module
import io.vrap.rmf.codegen.rendering.CodeGenerator
import io.vrap.rmf.codegen.rendering.FileGenerator

object BrunoModelModule : Module {
override fun configure(generatorModule: RamlGeneratorModule) = setOf<CodeGenerator>(
FileGenerator(
setOf(
BrunoModuleRenderer(generatorModule.provideRamlModel(), generatorModule.vrapTypeProvider()),
BrunoMethodRenderer(generatorModule.provideRamlModel(), generatorModule.vrapTypeProvider()),
BrunoActionRenderer(generatorModule.provideRamlModel(), generatorModule.vrapTypeProvider())
)
)
)
}
Loading

0 comments on commit 973d2c8

Please sign in to comment.