Skip to content

Commit

Permalink
Add support for pattern-matching (#15)
Browse files Browse the repository at this point in the history
Add support for pattern-matching
  • Loading branch information
2m authored May 10, 2019
2 parents d846d81 + 8751161 commit f1ef519
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 38 deletions.
93 changes: 60 additions & 33 deletions src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,59 @@ import com.lightbend.paradox.markdown.InlineDirective
import org.pegdown.Printer
import org.pegdown.ast.{DirectiveNode, TextNode, Visitor}

class ApidocDirective(allClasses: IndexedSeq[String]) extends InlineDirective("apidoc") {
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit =
if (node.label.split('[')(0).contains('.')) {
val fqcn = node.label
if (allClasses.contains(fqcn)) {
val label = fqcn.split('.').last
syntheticNode("scala", scalaLabel(label), fqcn, node).accept(visitor)
syntheticNode("java", javaLabel(label), fqcn, node).accept(visitor)
} else {
throw new java.lang.IllegalStateException(s"fqcn not found by @apidoc[$fqcn]")
class ApidocDirective(allClassesAndObjects: IndexedSeq[String]) extends InlineDirective("apidoc") {
val allClasses = allClassesAndObjects.filterNot(_.endsWith("$"))

private case class Query(pattern: String, generics: String, linkToObject: Boolean) {

def scalaLabel(matched: String): String =
matched.split('.').last + generics
def javaLabel(matched: String): String =
scalaLabel(matched)
.replaceAll("\\[", "<")
.replaceAll("\\]", ">")
.replaceAll("_", "?")

override def toString =
if (linkToObject) pattern + "$" + generics
else pattern + generics
}
private object Query {
def apply(label: String): Query = {
val (pattern, generics) = label.indexOf('[') match {
case -1 => (label, "")
case n => label.replaceAll("\\\\_", "_").splitAt(n)
}
} else {
renderByClassName(node.label, node, visitor, printer)
if (pattern.endsWith("$"))
Query(pattern.init, generics, linkToObject = true)
else
Query(pattern, generics, linkToObject = false)
}

private def baseClassName(label: String) = {
val labelWithoutGenerics = label.split("\\[")(0)
if (labelWithoutGenerics.endsWith("$")) labelWithoutGenerics.init
else labelWithoutGenerics
}

def javaLabel(label: String): String =
scalaLabel(label).replaceAll("\\[", "<").replaceAll("\\]", ">").replace('_', '?')

def scalaLabel(label: String): String =
if (label.endsWith("$")) label.init
else label
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
val query = Query(node.label)
if (query.pattern.contains('.')) {
if (allClasses.contains(query.pattern)) {
renderMatches(query, Seq(query.pattern), node, visitor, printer)
} else
allClasses.filter(_.contains(query.pattern)) match {
case Seq() =>
// No matches? then try globbing
val regex = (query.pattern.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$").r
allClasses.filter(cls => regex.findFirstMatchIn(cls).isDefined) match {
case Seq() =>
throw new java.lang.IllegalStateException(s"Class not found for @apidoc[$query]")
case results =>
renderMatches(query, results, node, visitor, printer)
}
case results =>
renderMatches(query, results, node, visitor, printer)
}
} else {
renderMatches(query, allClasses.filter(_.endsWith('.' + query.pattern)), node, visitor, printer)
}
}

def syntheticNode(group: String, label: String, fqcn: String, node: DirectiveNode): DirectiveNode = {
val syntheticSource = new DirectiveNode.Source.Direct(fqcn)
Expand All @@ -68,31 +94,32 @@ class ApidocDirective(allClasses: IndexedSeq[String]) extends InlineDirective("a
)
}

def renderByClassName(label: String, node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
val query = node.label.replaceAll("\\\\_", "_")
val className = baseClassName(query)
val scalaClassSuffix = if (query.endsWith("$")) "$" else ""
def renderMatches(query: Query,
matches: Seq[String],
node: DirectiveNode,
visitor: Visitor,
printer: Printer): Unit = {
val scalaClassSuffix = if (query.linkToObject) "$" else ""

val matches = allClasses.filter(_.endsWith('.' + className))
matches.size match {
case 0 =>
throw new java.lang.IllegalStateException(s"No matches found for $query")
case 1 if matches(0).contains("adsl") =>
throw new java.lang.IllegalStateException(s"Match for $query only found in one language: ${matches(0)}")
case 1 =>
syntheticNode("scala", scalaLabel(query), matches(0) + scalaClassSuffix, node).accept(visitor)
syntheticNode("java", javaLabel(query), matches(0), node).accept(visitor)
syntheticNode("scala", query.scalaLabel(matches(0)), matches(0) + scalaClassSuffix, node).accept(visitor)
syntheticNode("java", query.javaLabel(matches(0)), matches(0), node).accept(visitor)
case 2 if matches.forall(_.contains("adsl")) =>
matches.foreach(m => {
if (!m.contains("javadsl"))
syntheticNode("scala", scalaLabel(query), m + scalaClassSuffix, node).accept(visitor)
syntheticNode("scala", query.scalaLabel(m), m + scalaClassSuffix, node).accept(visitor)
if (!m.contains("scaladsl"))
syntheticNode("java", javaLabel(query), m, node).accept(visitor)
syntheticNode("java", query.javaLabel(m), m, node).accept(visitor)
})
case n =>
throw new java.lang.IllegalStateException(
s"$n matches found for $query, but not javadsl/scaladsl: ${matches.mkString(", ")}. " +
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[${label}]."
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[$query]."
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
"akka.actor.typed.ActorRef",
"akka.cluster.client.ClusterClient",
"akka.cluster.client.ClusterClient$",
"akka.cluster.ddata.Replicator",
"akka.cluster.ddata.Replicator$",
"akka.cluster.ddata.typed.scaladsl.Replicator",
"akka.cluster.ddata.typed.scaladsl.Replicator$",
"akka.cluster.ddata.typed.javadsl.Replicator",
"akka.cluster.ddata.typed.javadsl.Replicator$",
"akka.dispatch.Envelope",
"akka.http.javadsl.model.sse.ServerSentEvent",
"akka.http.javadsl.marshalling.Marshaller",
Expand All @@ -39,7 +45,7 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
"akka.stream.javadsl.Flow",
"akka.stream.javadsl.Flow$",
"akka.stream.scaladsl.Flow",
"akka.stream.scaladsl.Flow$"
"akka.stream.scaladsl.Flow$",
)

override val markdownWriter = new Writer(
Expand Down Expand Up @@ -86,6 +92,26 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
)
}

it should "allow linking to a typed class that is also present in classic" in {
markdown("@apidoc[typed.*.Replicator$]") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/akka/2.5/akka/cluster/ddata/typed/scaladsl/Replicator$.html">Replicator</a></span><span class="group-java">
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/cluster/ddata/typed/javadsl/Replicator.html">Replicator</a></span>
|</p>""".stripMargin
)
}

it should "allow linking to a classic class that is also present in typed" in {
markdown("@apidoc[ddata.Replicator$]") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/akka/2.5/akka/cluster/ddata/Replicator$.html">Replicator</a></span><span class="group-java">
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/cluster/ddata/Replicator.html">Replicator</a></span>
|</p>""".stripMargin
)
}

it should "throw an exception when two matches found but javadsl/scaladsl is not in their packages" in {
val thrown = the[IllegalStateException] thrownBy markdown("@apidoc[ActorRef]")
thrown.getMessage shouldEqual
Expand All @@ -102,10 +128,14 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
)
}

it should "throw an exception when `.` is in the [label], but the label is not fqcn" in {
val thrown = the[IllegalStateException] thrownBy markdown("@apidoc[actor.typed.ActorRef]")
thrown.getMessage shouldEqual
"fqcn not found by @apidoc[actor.typed.ActorRef]"
it should "find a class by partiql fqdn" in {
markdown("@apidoc[actor.typed.ActorRef]") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/akka/2.5/akka/actor/typed/ActorRef.html">ActorRef</a></span><span class="group-java">
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/actor/typed/ActorRef.html">ActorRef</a></span>
|</p>""".stripMargin
)
}

it should "generate markdown correctly for a companion object" in {
Expand Down

0 comments on commit f1ef519

Please sign in to comment.