bsp.codegen.bsp4j.JavaRenderer.scala Maven / Gradle / Ivy
The newest version!
package bsp.codegen.bsp4j
import bsp.codegen._
import bsp.codegen.dsl.{block, empty, lines, newline}
import bsp.codegen.ir.Def._
import bsp.codegen.ir.EnumType.{IntEnum, StringEnum}
import bsp.codegen.ir.Hint._
import bsp.codegen.ir.JsonRPCMethodType.{Notification, Request}
import bsp.codegen.ir.Primitive._
import bsp.codegen.ir.Type._
import bsp.codegen.ir._
import cats.implicits.toFoldableOps
import os.RelPath
import software.amazon.smithy.model.shapes.ShapeId
class JavaRenderer(basepkg: String, definitions: List[Def], version: String) {
import bsp.codegen.Settings.java
val baseRelPath: RelPath = os.rel / basepkg.split('.')
def render(): List[CodegenFile] = {
definitions.flatMap(renderDef) ++ List(renderVersion(), copyPreconditions())
}
def copyPreconditions(): CodegenFile = {
val preconditionsSourcePath =
os.pwd / "codegen" / "src" / "main" / "resources" / "Preconditions.java"
// TODO: dehardcode "codegen" path above
val preconditionsContents = os.read(preconditionsSourcePath)
val preconditionsPath = os.rel / "org" / "eclipse" / "lsp4j" / "util" / "Preconditions.java"
// For some reason extend expects this file to be present in this specific location,
// it can be removed once we stop using extend
CodegenFile(preconditionsPath, preconditionsContents)
}
def renderVersion(): CodegenFile = {
val contents = lines(
s"package $basepkg;",
newline,
block("public class Bsp4j")(
s"""public static final String PROTOCOL_VERSION = new String("$version");"""
),
newline
)
CodegenFile(baseRelPath / "Bsp4j.java", contents.render)
}
def renderDef(definition: Def): Option[CodegenFile] = {
definition match {
case Alias(shapeId, tpe, _) => None
case Structure(shapeId, fields, hints, _) => Some(renderStructure(shapeId, fields, hints))
case ClosedEnum(shapeId, enumType, values, hints) =>
Some(renderClosedEnum(shapeId, enumType, values, hints))
case OpenEnum(shapeId, enumType, values, hints) =>
Some(renderOpenEnum(shapeId, enumType, values, hints))
case Service(shapeId, operations, _) => Some(renderService(shapeId, operations))
}
}
def renderStructure(shapeId: ShapeId, fields: List[Field], hints: List[Hint]): CodegenFile = {
val requiredFields = fields.filter(_.required)
val docsLines = renderDocs(hints)
val allLines = lines(
renderPkg(shapeId),
newline,
"import org.eclipse.lsp4j.generator.JsonRpcData",
renderImports(fields),
newline,
docsLines,
"@JsonRpcData",
block(s"class ${shapeId.getName()}")(
lines(fields.map(renderJavaField)),
newline, {
val params = requiredFields.map(renderParam).mkString(", ")
val assignments = requiredFields.map(_.name).map(n => s"this.$n = $n")
block(s"new($params)")(assignments)
}
),
newline
)
val fileName = shapeId.getName() + ".xtend"
CodegenFile(baseRelPath / fileName, allLines.render)
}
def spreadEnumLines[A](enumType: EnumType[A], values: List[EnumValue[A]]): Lines = {
val renderedValues = values.map(renderEnumValueDef(enumType))
renderedValues.init.map(_ + ",") :+ (renderedValues.last + ";")
}
def renderDocs(hints: List[Hint]): Lines = {
val isUnstable = hints.contains(Unstable)
val unstableNote = if (isUnstable) {
List("**Unstable** (may change in future versions)")
} else {
List.empty
}
val docs = unstableNote ++
hints.collect { case Documentation(string) =>
string.split(System.lineSeparator()).toList
}.flatten
docs match {
case Nil => empty
case _ =>
lines(
"/**",
docs.map(line => s" * $line"),
" */"
)
}
}
def renderClosedEnum[A](
shapeId: ShapeId,
enumType: EnumType[A],
values: List[EnumValue[A]],
hints: List[Hint]
): CodegenFile = {
val evt = enumValueType(enumType)
val tpe = shapeId.getName()
val docsLines = renderDocs(hints)
val allLines = lines(
renderPkg(shapeId).map(_ + ";"),
newline,
"import com.google.gson.annotations.JsonAdapter;",
"import org.eclipse.lsp4j.jsonrpc.json.adapters.EnumTypeAdapter;",
newline,
docsLines,
"@JsonAdapter(EnumTypeAdapter.Factory.class)",
block(s"public enum $tpe")(
newline,
spreadEnumLines(enumType, values),
newline,
s"private final $evt value;",
newline,
block(s"$tpe($evt value)") {
"this.value = value;"
},
newline,
block(s"public $evt getValue()") {
"return value;"
},
newline,
block(s"public static $tpe forValue($evt value)")(
s"$tpe[] allValues = $tpe.values();",
"if (value < 1 || value > allValues.length)",
lines("""throw new IllegalArgumentException("Illegal enum value: " + value);""").indent,
"return allValues[value - 1];"
)
),
newline
)
val fileName = shapeId.getName() + ".java"
CodegenFile(baseRelPath / fileName, allLines.render)
}
def renderOpenEnum[A](
shapeId: ShapeId,
enumType: EnumType[A],
values: List[EnumValue[A]],
hints: List[Hint]
): CodegenFile = {
val tpe = shapeId.getName()
val docsLines = renderDocs(hints)
val allLines = lines(
renderPkg(shapeId).map(_ + ";"),
newline,
docsLines,
block(s"public class $tpe") {
values.map(renderStaticValue(enumType))
},
newline
)
val fileName = shapeId.getName() + ".java"
CodegenFile(baseRelPath / fileName, allLines.render)
}
def renderService(shapeId: ShapeId, operations: List[Operation]): CodegenFile = {
val allLines = lines(
renderPkg(shapeId).map(_ + ";"),
newline,
"import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;",
"import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;",
newline,
"import java.util.concurrent.CompletableFuture;",
newline,
block(s"public interface ${shapeId.getName()}")(
operations.foldMap(renderOperation),
newline
),
newline
)
val fileName = shapeId.getName() + ".java"
CodegenFile(baseRelPath / fileName, allLines.render)
}
def renderOperation(operation: Operation): Lines = {
val output = (operation.jsonRPCMethodType, operation.outputType) match {
case (Notification, _) => "void"
case (Request, TPrimitive(Primitive.PUnit, _)) => s"CompletableFuture