Skip to content

Commit f1ef519

Browse files
authored
Add support for pattern-matching (#15)
Add support for pattern-matching
2 parents d846d81 + 8751161 commit f1ef519

File tree

2 files changed

+95
-38
lines changed

2 files changed

+95
-38
lines changed

src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,59 @@ import com.lightbend.paradox.markdown.InlineDirective
2020
import org.pegdown.Printer
2121
import org.pegdown.ast.{DirectiveNode, TextNode, Visitor}
2222

23-
class ApidocDirective(allClasses: IndexedSeq[String]) extends InlineDirective("apidoc") {
24-
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit =
25-
if (node.label.split('[')(0).contains('.')) {
26-
val fqcn = node.label
27-
if (allClasses.contains(fqcn)) {
28-
val label = fqcn.split('.').last
29-
syntheticNode("scala", scalaLabel(label), fqcn, node).accept(visitor)
30-
syntheticNode("java", javaLabel(label), fqcn, node).accept(visitor)
31-
} else {
32-
throw new java.lang.IllegalStateException(s"fqcn not found by @apidoc[$fqcn]")
23+
class ApidocDirective(allClassesAndObjects: IndexedSeq[String]) extends InlineDirective("apidoc") {
24+
val allClasses = allClassesAndObjects.filterNot(_.endsWith("$"))
25+
26+
private case class Query(pattern: String, generics: String, linkToObject: Boolean) {
27+
28+
def scalaLabel(matched: String): String =
29+
matched.split('.').last + generics
30+
def javaLabel(matched: String): String =
31+
scalaLabel(matched)
32+
.replaceAll("\\[", "<")
33+
.replaceAll("\\]", ">")
34+
.replaceAll("_", "?")
35+
36+
override def toString =
37+
if (linkToObject) pattern + "$" + generics
38+
else pattern + generics
39+
}
40+
private object Query {
41+
def apply(label: String): Query = {
42+
val (pattern, generics) = label.indexOf('[') match {
43+
case -1 => (label, "")
44+
case n => label.replaceAll("\\\\_", "_").splitAt(n)
3345
}
34-
} else {
35-
renderByClassName(node.label, node, visitor, printer)
46+
if (pattern.endsWith("$"))
47+
Query(pattern.init, generics, linkToObject = true)
48+
else
49+
Query(pattern, generics, linkToObject = false)
3650
}
37-
38-
private def baseClassName(label: String) = {
39-
val labelWithoutGenerics = label.split("\\[")(0)
40-
if (labelWithoutGenerics.endsWith("$")) labelWithoutGenerics.init
41-
else labelWithoutGenerics
4251
}
4352

44-
def javaLabel(label: String): String =
45-
scalaLabel(label).replaceAll("\\[", "<").replaceAll("\\]", ">").replace('_', '?')
46-
47-
def scalaLabel(label: String): String =
48-
if (label.endsWith("$")) label.init
49-
else label
53+
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
54+
val query = Query(node.label)
55+
if (query.pattern.contains('.')) {
56+
if (allClasses.contains(query.pattern)) {
57+
renderMatches(query, Seq(query.pattern), node, visitor, printer)
58+
} else
59+
allClasses.filter(_.contains(query.pattern)) match {
60+
case Seq() =>
61+
// No matches? then try globbing
62+
val regex = (query.pattern.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$").r
63+
allClasses.filter(cls => regex.findFirstMatchIn(cls).isDefined) match {
64+
case Seq() =>
65+
throw new java.lang.IllegalStateException(s"Class not found for @apidoc[$query]")
66+
case results =>
67+
renderMatches(query, results, node, visitor, printer)
68+
}
69+
case results =>
70+
renderMatches(query, results, node, visitor, printer)
71+
}
72+
} else {
73+
renderMatches(query, allClasses.filter(_.endsWith('.' + query.pattern)), node, visitor, printer)
74+
}
75+
}
5076

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

71-
def renderByClassName(label: String, node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
72-
val query = node.label.replaceAll("\\\\_", "_")
73-
val className = baseClassName(query)
74-
val scalaClassSuffix = if (query.endsWith("$")) "$" else ""
97+
def renderMatches(query: Query,
98+
matches: Seq[String],
99+
node: DirectiveNode,
100+
visitor: Visitor,
101+
printer: Printer): Unit = {
102+
val scalaClassSuffix = if (query.linkToObject) "$" else ""
75103

76-
val matches = allClasses.filter(_.endsWith('.' + className))
77104
matches.size match {
78105
case 0 =>
79106
throw new java.lang.IllegalStateException(s"No matches found for $query")
80107
case 1 if matches(0).contains("adsl") =>
81108
throw new java.lang.IllegalStateException(s"Match for $query only found in one language: ${matches(0)}")
82109
case 1 =>
83-
syntheticNode("scala", scalaLabel(query), matches(0) + scalaClassSuffix, node).accept(visitor)
84-
syntheticNode("java", javaLabel(query), matches(0), node).accept(visitor)
110+
syntheticNode("scala", query.scalaLabel(matches(0)), matches(0) + scalaClassSuffix, node).accept(visitor)
111+
syntheticNode("java", query.javaLabel(matches(0)), matches(0), node).accept(visitor)
85112
case 2 if matches.forall(_.contains("adsl")) =>
86113
matches.foreach(m => {
87114
if (!m.contains("javadsl"))
88-
syntheticNode("scala", scalaLabel(query), m + scalaClassSuffix, node).accept(visitor)
115+
syntheticNode("scala", query.scalaLabel(m), m + scalaClassSuffix, node).accept(visitor)
89116
if (!m.contains("scaladsl"))
90-
syntheticNode("java", javaLabel(query), m, node).accept(visitor)
117+
syntheticNode("java", query.javaLabel(m), m, node).accept(visitor)
91118
})
92119
case n =>
93120
throw new java.lang.IllegalStateException(
94121
s"$n matches found for $query, but not javadsl/scaladsl: ${matches.mkString(", ")}. " +
95-
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[${label}]."
122+
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[$query]."
96123
)
97124
}
98125
}

src/test/scala/com.lightbend.paradox/apidoc/ApidocDirectiveSpec.scala

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
2626
"akka.actor.typed.ActorRef",
2727
"akka.cluster.client.ClusterClient",
2828
"akka.cluster.client.ClusterClient$",
29+
"akka.cluster.ddata.Replicator",
30+
"akka.cluster.ddata.Replicator$",
31+
"akka.cluster.ddata.typed.scaladsl.Replicator",
32+
"akka.cluster.ddata.typed.scaladsl.Replicator$",
33+
"akka.cluster.ddata.typed.javadsl.Replicator",
34+
"akka.cluster.ddata.typed.javadsl.Replicator$",
2935
"akka.dispatch.Envelope",
3036
"akka.http.javadsl.model.sse.ServerSentEvent",
3137
"akka.http.javadsl.marshalling.Marshaller",
@@ -39,7 +45,7 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
3945
"akka.stream.javadsl.Flow",
4046
"akka.stream.javadsl.Flow$",
4147
"akka.stream.scaladsl.Flow",
42-
"akka.stream.scaladsl.Flow$"
48+
"akka.stream.scaladsl.Flow$",
4349
)
4450

4551
override val markdownWriter = new Writer(
@@ -86,6 +92,26 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
8692
)
8793
}
8894

95+
it should "allow linking to a typed class that is also present in classic" in {
96+
markdown("@apidoc[typed.*.Replicator$]") shouldEqual
97+
html(
98+
"""<p><span class="group-scala">
99+
|<a href="https://doc.akka.io/api/akka/2.5/akka/cluster/ddata/typed/scaladsl/Replicator$.html">Replicator</a></span><span class="group-java">
100+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/cluster/ddata/typed/javadsl/Replicator.html">Replicator</a></span>
101+
|</p>""".stripMargin
102+
)
103+
}
104+
105+
it should "allow linking to a classic class that is also present in typed" in {
106+
markdown("@apidoc[ddata.Replicator$]") shouldEqual
107+
html(
108+
"""<p><span class="group-scala">
109+
|<a href="https://doc.akka.io/api/akka/2.5/akka/cluster/ddata/Replicator$.html">Replicator</a></span><span class="group-java">
110+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/cluster/ddata/Replicator.html">Replicator</a></span>
111+
|</p>""".stripMargin
112+
)
113+
}
114+
89115
it should "throw an exception when two matches found but javadsl/scaladsl is not in their packages" in {
90116
val thrown = the[IllegalStateException] thrownBy markdown("@apidoc[ActorRef]")
91117
thrown.getMessage shouldEqual
@@ -102,10 +128,14 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
102128
)
103129
}
104130

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

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

0 commit comments

Comments
 (0)