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

bsp.codegen.docs.MarkdownRenderer.scala Maven / Gradle / Ivy

The newest version!
package bsp.codegen.docs

import bsp.codegen._
import bsp.codegen.ir.Hint.{Documentation, Deprecated}
import bsp.codegen.ir.JsonRPCMethodType.{Notification, Request}
import bsp.codegen.ir.{Def, Hint, Operation, PolymorphicDataKind}
import cats.syntax.all._
import software.amazon.smithy.model.shapes.ShapeId

import scala.collection.mutable.{Set => MSet}

object MarkdownRenderer {
  def render(tree: DocTree): String = {
    val visited = MSet.empty[ShapeId]
    val version = ProtocolVersionLoader.version()
    val renderer = new MarkdownRenderer(tree, visited, version)
    val rendered = renderer.render.render

    val notRenderedIds = visited.diff(tree.structures.keys.toSet)
    notRenderedIds.foreach { id =>
      println(s"warning: ${id.getName} was not rendered")
    }

    rendered
  }
}

class MarkdownRenderer private (tree: DocTree, visited: MSet[ShapeId], protocolVersion: String) {
  import bsp.codegen.Settings.typescript
  import dsl._

  val tsRenderer = new TypescriptRenderer

  def render: Lines = {
    val commonShapes =
      if (tree.commonShapeIds.isEmpty) empty
      else
        lines(
          "## Common shapes",
          newline,
          tree.commonShapeIds.foldMap(renderStructureNode),
          newline
        )

    // Server comes first because it's bigger.
    val services = tree.services.sortBy(_.operations.size).reverse.foldMap(renderServiceNode)

    val dataKinds = tree.dataKindInhabitants.toList.foldMap { case (extendableType, kinds) =>
      renderExtendableTypeNode(extendableType, kinds)
    }

    val versionRendered = lines(
      "## BSP version",
      s"`$protocolVersion`",
      newline
    )

    lines(
      versionRendered,
      commonShapes,
      services,
      dataKinds
    )
  }

  def renderStructureNode(id: ShapeId): Lines = {
    // We don't want to generate definitions for built-in primitives.
    if (id.getNamespace == "smithy.api") {
      return empty
    }

    val node = tree.structures(id)

    if (visited.contains(id)) empty
    else {
      visited.add(id)
      lines(
        renderStructureNodeDef(node.definition),
        node.definition.members
          .filter(_.getNamespace == id.getNamespace)
          .foldMap(renderStructureNode)
      )
    }
  }

  def renderStructureNodeDef(definition: Def): Lines = {
    lines(
      s"#### ${definition.shapeId.getName()}",
      newline,
      documentation(definition.hints),
      newline,
      tsBlock(definition),
      newline
    )
  }

  def renderExtendableTypeNode(extendableType: ShapeId, kinds: List[PolymorphicDataKind]): Lines = {
    val header = s"## ${extendableType.getName()} kinds"
    val kindNodes = kinds.foldMap(renderDataKindNode(extendableType))
    lines(
      header,
      newline,
      kindNodes
    )
  }

  def renderDataKindNode(extendableType: ShapeId)(kind: PolymorphicDataKind): Lines = {
    lines(
      s"### ${kind.shapeId.getName()}",
      "This structure is embedded in",
      s"the `data?: ${extendableType.getName()}` field, when",
      s"""the `dataKind` field contains `"${kind.kind}"`.""",
      newline,
      renderStructureNode(kind.shapeId)
    )
  }

  def renderOperationNode(node: OperationDocNode): Lines = {
    val OperationDocNode(operation, input, output) = node
    lines(
      s"### ${operation.name}: ${methodTpe(operation)}",
      newline,
      documentation(operation.hints),
      newline,
      s"- method: `${operation.jsonRPCMethod}`",
      input.foldMap(n => s"- params: `${n.getName()}`"),
      output.foldMap(n => s"- result: `${n.getName()}`"),
      newline,
      input.foldMap(renderStructureNode),
      output.foldMap(renderStructureNode)
    )
  }

  def renderServiceNode(node: ServiceDocNode): Lines = {
    val ServiceDocNode(shapeId, operations) = node
    val header = if (shapeId.getName().toLowerCase().contains("server")) {
      s"## BSP Server remote interface"
    } else {
      s"## BSP Client remote interface"
    }
    lines(
      header,
      newline,
      operations
        .map(tree.operations)
        .foldMap(renderOperationNode)
    )
  }

  def methodTpe(operation: Operation): String = operation.jsonRPCMethodType match {
    case Notification => "notification"
    case Request      => "request"
  }

  def documentation(hints: List[Hint]): Lines = Lines {
    val maybeDeprecated = hints.collectFirst { case Deprecated(message) =>
      val line = if (message.isEmpty) List("**Deprecated**") else List(s"**Deprecated**: $message")
      lines(
        line,
        newline
      )
    }

    val maybeUnstable = hints.collectFirst { case Hint.Unstable =>
      lines(
        List("**Unstable** (may change in future versions)"),
        newline
      )
    }

    val docs = hints.collect { case Documentation(string) =>
      string.split(System.lineSeparator()).toList
    }

    lines(
      maybeDeprecated,
      maybeUnstable,
      docs
    )
  }

  def tsBlock(definition: Def): Lines =
    tsRenderer
      .render(definition)
      .map { tsCode =>
        lines(
          "```ts",
          tsCode,
          "```"
        )
      }
      .getOrElse(empty)

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy