diff --git a/src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala b/src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala index 63dabff..0efd720 100644 --- a/src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala +++ b/src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala @@ -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) @@ -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]." ) } } diff --git a/src/test/scala/com.lightbend.paradox/apidoc/ApidocDirectiveSpec.scala b/src/test/scala/com.lightbend.paradox/apidoc/ApidocDirectiveSpec.scala index 87f1dee..085f01c 100644 --- a/src/test/scala/com.lightbend.paradox/apidoc/ApidocDirectiveSpec.scala +++ b/src/test/scala/com.lightbend.paradox/apidoc/ApidocDirectiveSpec.scala @@ -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", @@ -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( @@ -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( + """
+ |Replicator + |Replicator + |
""".stripMargin + ) + } + + it should "allow linking to a classic class that is also present in typed" in { + markdown("@apidoc[ddata.Replicator$]") shouldEqual + html( + """+ |Replicator + |Replicator + |
""".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 @@ -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( + """""".stripMargin + ) } it should "generate markdown correctly for a companion object" in {