Skip to content

Commit

Permalink
Find and render inner classes (#98)
Browse files Browse the repository at this point in the history
* Find and render inner classes

* Support regex for inner classes
  • Loading branch information
ennru authored and raboof committed Dec 4, 2019
1 parent 989d333 commit 8474df5
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 17 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ produce an unambigious result, you will have to use the FQCN.
* Scala: Flow - `akka/stream/scaladsl/Flow.html`
* Java: Flow - `akka/stream/javadsl/Flow.html`

* `@apidoc[Receptionist.Command]` (An inner class.)
* classes: `akka.actor.typed.receptionist.Receptionist$Command`
* Scala: Receptionist.Command - `akka/actor/typed/receptionist/Receptionist$$Command.html`
* Java: Receptionist.Command - `akka/actor/typed/receptionist/Receptionist.Command.html`

* `@apidoc[Marshaller]` (The scaladoc/javadoc split can be on different package depth.)
* classes: `akka.http.scaladsl.marshalling.Marshaller`, `akka.http.javadsl.marshalling.Marshaller`
* Scala: Marshaller - `akka/http/scaladsl/marshalling/Marshaller.html`
Expand All @@ -58,7 +63,7 @@ produce an unambigious result, you will have to use the FQCN.
* Scala: ClusterClient - `akka/cluster/client/ClusterClient$.html`
* Java: ClusterClient - `akka/cluster/client/ClusterClient.html`

* `@apidoc[Source[ServerSentEvent, \_]]` (Show type paramters.)
* `@apidoc[Source[ServerSentEvent, \_]]` (Show type parameters.)
* classes: `akka.stream.scaladsl.Source` - `akka.stream.javadsl.Source`
* Scala: Source\[ServerSentEvent, _\] - `akka/stream/scaladsl/Source.html`
* Java: Source\<ServerSentEvent, ?\> - `akka/stream/javadsl/Source.html`
Expand Down
59 changes: 46 additions & 13 deletions src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import org.pegdown.Printer
import org.pegdown.ast.DirectiveNode.Source
import org.pegdown.ast.{DirectiveNode, Visitor}

import scala.util.matching.Regex

class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Context) extends InlineDirective("apidoc") {
final val JavadocProperty = raw"""javadoc\.(.*)\.base_url""".r
final val JavadocBaseUrls = ctx.properties.collect {
Expand All @@ -33,16 +35,21 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
private case class Query(label: Option[String], pattern: String, generics: String, linkToObject: Boolean) {
def scalaLabel(matched: String): String =
label match {
case None => matched.split('.').last + generics
case None => matched.split('.').last.replace("$", ".") + generics
case Some(la) => la + generics
}

def scalaFqcn(matched: String): String =
matched.replace("$", ".")

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

def javaFqcn(matched: String): String = scalaFqcn(matched)

override def toString =
if (linkToObject) pattern + "$" + generics
else pattern + generics
Expand Down Expand Up @@ -77,27 +84,50 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
case s: Source.Direct => Query(node.label, s.value)
}
if (query.pattern.contains('.')) {
if (allClasses.contains(query.pattern)) {
val classNameWithDollarForInnerClasses = query.pattern.replaceAll("(\\b[A-Z].+)\\.", "$1\\$")
if (allClasses.contains(classNameWithDollarForInnerClasses)) {
renderMatches(query, Seq(query.pattern), node, visitor, printer)
} else
allClasses.filter(_.contains(query.pattern)) match {
} else {
allClasses.filter(_.contains(classNameWithDollarForInnerClasses)) match {
case Seq() =>
// No matches? then try globbing
val regex = (query.pattern.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$").r
val regex = convertToRegex(classNameWithDollarForInnerClasses)
allClasses.filter(cls => regex.findFirstMatchIn(cls).isDefined) match {
case Seq() =>
ctx.error(s"Class not found for @apidoc[$query]", node)
ctx.error(s"Class not found for @apidoc[$query] (pattern $regex)", node)
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)
}
}

private def convertToRegex(classNameWithDollarForInnerClasses: String): Regex =
(classNameWithDollarForInnerClasses
.replaceAll("\\.", "\\\\.")
.replaceAll("\\*", ".*")
.replaceAll("\\$", "\\\\\\$") + "$").r

private def scaladocNode(
group: String,
label: String,
fqcn: String,
anchor: String,
node: DirectiveNode
): DirectiveNode = syntheticNode(group, "scala", label, fqcn, anchor, node)

private def javadocNode(
label: String,
fqcn: String,
anchor: String,
node: DirectiveNode
): DirectiveNode = syntheticNode("java", "java", label, fqcn, anchor, node)

private def syntheticNode(
group: String,
doctype: String,
Expand Down Expand Up @@ -147,28 +177,31 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
)
case 1 =>
val pkg = matches(0)
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, sAnchor, node).accept(visitor)
scaladocNode("scala", query.scalaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, sAnchor, node)
.accept(visitor)
if (hasJavadocUrl(pkg)) {
syntheticNode("java", "java", query.javaLabel(pkg), pkg, jAnchor, node).accept(visitor)
javadocNode(query.javaLabel(pkg), query.javaFqcn(pkg), jAnchor, node).accept(visitor)
} else
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, jAnchor, node).accept(visitor)
scaladocNode("java", query.javaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, jAnchor, node)
.accept(visitor)
case 2 if matches.forall(_.contains("adsl")) =>
matches.foreach(pkg => {
if (!pkg.contains("javadsl"))
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, sAnchor, node)
scaladocNode("scala", query.scalaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, sAnchor, node)
.accept(visitor)
if (!pkg.contains("scaladsl")) {
if (hasJavadocUrl(pkg))
syntheticNode("java", "java", query.javaLabel(pkg), pkg, jAnchor, node).accept(visitor)
javadocNode(query.javaLabel(pkg), query.javaFqcn(pkg), jAnchor, node).accept(visitor)
else
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, jAnchor, node)
scaladocNode("java", query.javaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, jAnchor, node)
.accept(visitor)
}
})
case n =>
ctx.error(
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[$query].",
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[$query]. " +
s"For examples see https://github.com/lightbend/sbt-paradox-apidoc#examples",
node
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
"akka.stream.javadsl.Flow",
"akka.stream.javadsl.Flow$",
"akka.stream.scaladsl.Flow",
"akka.stream.scaladsl.Flow$"
"akka.stream.scaladsl.Flow$",
"akka.kafka.scaladsl.Consumer$Control",
"akka.kafka.javadsl.Consumer$Control",
"akka.actor.typed.receptionist.Receptionist$Command"
)

override val markdownWriter = new Writer(
Expand All @@ -64,7 +67,9 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
"scaladoc.akka.base_url" -> "https://doc.akka.io/api/akka/2.5",
"scaladoc.akka.http.base_url" -> "https://doc.akka.io/api/akka-http/current",
"javadoc.akka.base_url" -> "https://doc.akka.io/japi/akka/2.5",
"javadoc.akka.http.base_url" -> "https://doc.akka.io/japi/akka-http/current"
"javadoc.akka.http.base_url" -> "https://doc.akka.io/japi/akka-http/current",
"scaladoc.akka.kafka.base_url" -> "https://doc.akka.io/api/alpakka-kafka/current",
"javadoc.akka.kafka.base_url" -> ""
)

"Apidoc directive" should "generate markdown correctly when there is only one match" in {
Expand Down Expand Up @@ -116,7 +121,7 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
it should "throw an exception when two matches found but javadsl/scaladsl is not in their packages" in {
val thrown = the[ParadoxException] thrownBy markdown("@apidoc[ActorRef]")
thrown.getMessage shouldEqual
"2 matches found for ActorRef, but not javadsl/scaladsl: akka.actor.ActorRef, akka.actor.typed.ActorRef. You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[ActorRef]."
"2 matches found for ActorRef, but not javadsl/scaladsl: akka.actor.ActorRef, akka.actor.typed.ActorRef. You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[ActorRef]. For examples see https://github.com/lightbend/sbt-paradox-apidoc#examples"
}

it should "generate markdown correctly when fully qualified class name (fqcn) is specified as @apidoc[fqcn]" in {
Expand Down Expand Up @@ -189,6 +194,56 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
)
}

"Inner classes" should "be linked (only scaladoc)" in {
markdown("@apidoc[Consumer.Control]") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/scaladsl/Consumer$$Control.html" title="akka.kafka.scaladsl.Consumer.Control"><code>Consumer.Control</code></a></span><span class="group-java">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/javadsl/Consumer$$Control.html" title="akka.kafka.javadsl.Consumer.Control"><code>Consumer.Control</code></a></span>
|</p>""".stripMargin
)
}

it should "be linked with a label and generics (only scaladoc)" in {
markdown("@apidoc[Consumer.Control[T]](Consumer.Control)") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/scaladsl/Consumer$$Control.html" title="akka.kafka.scaladsl.Consumer.Control"><code>Consumer.Control[T]</code></a></span><span class="group-java">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/javadsl/Consumer$$Control.html" title="akka.kafka.javadsl.Consumer.Control"><code>Consumer.Control&lt;T&gt;</code></a></span>
|</p>""".stripMargin
)
}

it should "be linked with a regex" in {
markdown("@apidoc[akka.kafka.(scaladsl|javadsl).Consumer.Control]") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/scaladsl/Consumer$$Control.html" title="akka.kafka.scaladsl.Consumer.Control"><code>Consumer.Control</code></a></span><span class="group-java">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/javadsl/Consumer$$Control.html" title="akka.kafka.javadsl.Consumer.Control"><code>Consumer.Control</code></a></span>
|</p>""".stripMargin
)
}

it should "be linked with a regex and label" in {
markdown("@apidoc[Consumer.Control](akka.kafka.(scaladsl|javadsl).Consumer.Control)") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/scaladsl/Consumer$$Control.html" title="akka.kafka.scaladsl.Consumer.Control"><code>Consumer.Control</code></a></span><span class="group-java">
|<a href="https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/javadsl/Consumer$$Control.html" title="akka.kafka.javadsl.Consumer.Control"><code>Consumer.Control</code></a></span>
|</p>""".stripMargin
)
}

it should "generate links to inner classes" in {
markdown("@apidoc[Receptionist.Command]") shouldEqual
html(
"""<p><span class="group-scala">
|<a href="https://doc.akka.io/api/akka/2.5/akka/actor/typed/receptionist/Receptionist$$Command.html" title="akka.actor.typed.receptionist.Receptionist.Command"><code>Receptionist.Command</code></a></span><span class="group-java">
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/actor/typed/receptionist/Receptionist.Command.html" title="akka.actor.typed.receptionist.Receptionist.Command"><code>Receptionist.Command</code></a></span>
|</p>""".stripMargin
)
}

"Directive with label and source" should "use the source as class pattern" in {
markdown("The @apidoc[TheClass.method](Flow) { .scaladoc a=1 } thingie") shouldEqual
html(
Expand Down

0 comments on commit 8474df5

Please sign in to comment.