Skip to content

Commit 8474df5

Browse files
ennruraboof
authored andcommitted
Find and render inner classes (#98)
* Find and render inner classes * Support regex for inner classes
1 parent 989d333 commit 8474df5

File tree

3 files changed

+110
-17
lines changed

3 files changed

+110
-17
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ produce an unambigious result, you will have to use the FQCN.
3838
* Scala: Flow - `akka/stream/scaladsl/Flow.html`
3939
* Java: Flow - `akka/stream/javadsl/Flow.html`
4040

41+
* `@apidoc[Receptionist.Command]` (An inner class.)
42+
* classes: `akka.actor.typed.receptionist.Receptionist$Command`
43+
* Scala: Receptionist.Command - `akka/actor/typed/receptionist/Receptionist$$Command.html`
44+
* Java: Receptionist.Command - `akka/actor/typed/receptionist/Receptionist.Command.html`
45+
4146
* `@apidoc[Marshaller]` (The scaladoc/javadoc split can be on different package depth.)
4247
* classes: `akka.http.scaladsl.marshalling.Marshaller`, `akka.http.javadsl.marshalling.Marshaller`
4348
* Scala: Marshaller - `akka/http/scaladsl/marshalling/Marshaller.html`
@@ -58,7 +63,7 @@ produce an unambigious result, you will have to use the FQCN.
5863
* Scala: ClusterClient - `akka/cluster/client/ClusterClient$.html`
5964
* Java: ClusterClient - `akka/cluster/client/ClusterClient.html`
6065

61-
* `@apidoc[Source[ServerSentEvent, \_]]` (Show type paramters.)
66+
* `@apidoc[Source[ServerSentEvent, \_]]` (Show type parameters.)
6267
* classes: `akka.stream.scaladsl.Source` - `akka.stream.javadsl.Source`
6368
* Scala: Source\[ServerSentEvent, _\] - `akka/stream/scaladsl/Source.html`
6469
* Java: Source\<ServerSentEvent, ?\> - `akka/stream/javadsl/Source.html`

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

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import org.pegdown.Printer
2222
import org.pegdown.ast.DirectiveNode.Source
2323
import org.pegdown.ast.{DirectiveNode, Visitor}
2424

25+
import scala.util.matching.Regex
26+
2527
class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Context) extends InlineDirective("apidoc") {
2628
final val JavadocProperty = raw"""javadoc\.(.*)\.base_url""".r
2729
final val JavadocBaseUrls = ctx.properties.collect {
@@ -33,16 +35,21 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
3335
private case class Query(label: Option[String], pattern: String, generics: String, linkToObject: Boolean) {
3436
def scalaLabel(matched: String): String =
3537
label match {
36-
case None => matched.split('.').last + generics
38+
case None => matched.split('.').last.replace("$", ".") + generics
3739
case Some(la) => la + generics
3840
}
3941

42+
def scalaFqcn(matched: String): String =
43+
matched.replace("$", ".")
44+
4045
def javaLabel(matched: String): String =
4146
scalaLabel(matched)
4247
.replaceAll("\\[", "<")
4348
.replaceAll("\\]", ">")
4449
.replaceAll("_", "?")
4550

51+
def javaFqcn(matched: String): String = scalaFqcn(matched)
52+
4653
override def toString =
4754
if (linkToObject) pattern + "$" + generics
4855
else pattern + generics
@@ -77,27 +84,50 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
7784
case s: Source.Direct => Query(node.label, s.value)
7885
}
7986
if (query.pattern.contains('.')) {
80-
if (allClasses.contains(query.pattern)) {
87+
val classNameWithDollarForInnerClasses = query.pattern.replaceAll("(\\b[A-Z].+)\\.", "$1\\$")
88+
if (allClasses.contains(classNameWithDollarForInnerClasses)) {
8189
renderMatches(query, Seq(query.pattern), node, visitor, printer)
82-
} else
83-
allClasses.filter(_.contains(query.pattern)) match {
90+
} else {
91+
allClasses.filter(_.contains(classNameWithDollarForInnerClasses)) match {
8492
case Seq() =>
8593
// No matches? then try globbing
86-
val regex = (query.pattern.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$").r
94+
val regex = convertToRegex(classNameWithDollarForInnerClasses)
8795
allClasses.filter(cls => regex.findFirstMatchIn(cls).isDefined) match {
8896
case Seq() =>
89-
ctx.error(s"Class not found for @apidoc[$query]", node)
97+
ctx.error(s"Class not found for @apidoc[$query] (pattern $regex)", node)
9098
case results =>
9199
renderMatches(query, results, node, visitor, printer)
92100
}
93101
case results =>
94102
renderMatches(query, results, node, visitor, printer)
95103
}
104+
}
96105
} else {
97106
renderMatches(query, allClasses.filter(_.endsWith('.' + query.pattern)), node, visitor, printer)
98107
}
99108
}
100109

110+
private def convertToRegex(classNameWithDollarForInnerClasses: String): Regex =
111+
(classNameWithDollarForInnerClasses
112+
.replaceAll("\\.", "\\\\.")
113+
.replaceAll("\\*", ".*")
114+
.replaceAll("\\$", "\\\\\\$") + "$").r
115+
116+
private def scaladocNode(
117+
group: String,
118+
label: String,
119+
fqcn: String,
120+
anchor: String,
121+
node: DirectiveNode
122+
): DirectiveNode = syntheticNode(group, "scala", label, fqcn, anchor, node)
123+
124+
private def javadocNode(
125+
label: String,
126+
fqcn: String,
127+
anchor: String,
128+
node: DirectiveNode
129+
): DirectiveNode = syntheticNode("java", "java", label, fqcn, anchor, node)
130+
101131
private def syntheticNode(
102132
group: String,
103133
doctype: String,
@@ -147,28 +177,31 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
147177
)
148178
case 1 =>
149179
val pkg = matches(0)
150-
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, sAnchor, node).accept(visitor)
180+
scaladocNode("scala", query.scalaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, sAnchor, node)
181+
.accept(visitor)
151182
if (hasJavadocUrl(pkg)) {
152-
syntheticNode("java", "java", query.javaLabel(pkg), pkg, jAnchor, node).accept(visitor)
183+
javadocNode(query.javaLabel(pkg), query.javaFqcn(pkg), jAnchor, node).accept(visitor)
153184
} else
154-
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, jAnchor, node).accept(visitor)
185+
scaladocNode("java", query.javaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, jAnchor, node)
186+
.accept(visitor)
155187
case 2 if matches.forall(_.contains("adsl")) =>
156188
matches.foreach(pkg => {
157189
if (!pkg.contains("javadsl"))
158-
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, sAnchor, node)
190+
scaladocNode("scala", query.scalaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, sAnchor, node)
159191
.accept(visitor)
160192
if (!pkg.contains("scaladsl")) {
161193
if (hasJavadocUrl(pkg))
162-
syntheticNode("java", "java", query.javaLabel(pkg), pkg, jAnchor, node).accept(visitor)
194+
javadocNode(query.javaLabel(pkg), query.javaFqcn(pkg), jAnchor, node).accept(visitor)
163195
else
164-
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, jAnchor, node)
196+
scaladocNode("java", query.javaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, jAnchor, node)
165197
.accept(visitor)
166198
}
167199
})
168200
case n =>
169201
ctx.error(
170202
s"$n matches found for $query, but not javadsl/scaladsl: ${matches.mkString(", ")}. " +
171-
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[$query].",
203+
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[$query]. " +
204+
s"For examples see https://github.com/lightbend/sbt-paradox-apidoc#examples",
172205
node
173206
)
174207
}

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

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
4646
"akka.stream.javadsl.Flow",
4747
"akka.stream.javadsl.Flow$",
4848
"akka.stream.scaladsl.Flow",
49-
"akka.stream.scaladsl.Flow$"
49+
"akka.stream.scaladsl.Flow$",
50+
"akka.kafka.scaladsl.Consumer$Control",
51+
"akka.kafka.javadsl.Consumer$Control",
52+
"akka.actor.typed.receptionist.Receptionist$Command"
5053
)
5154

5255
override val markdownWriter = new Writer(
@@ -64,7 +67,9 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
6467
"scaladoc.akka.base_url" -> "https://doc.akka.io/api/akka/2.5",
6568
"scaladoc.akka.http.base_url" -> "https://doc.akka.io/api/akka-http/current",
6669
"javadoc.akka.base_url" -> "https://doc.akka.io/japi/akka/2.5",
67-
"javadoc.akka.http.base_url" -> "https://doc.akka.io/japi/akka-http/current"
70+
"javadoc.akka.http.base_url" -> "https://doc.akka.io/japi/akka-http/current",
71+
"scaladoc.akka.kafka.base_url" -> "https://doc.akka.io/api/alpakka-kafka/current",
72+
"javadoc.akka.kafka.base_url" -> ""
6873
)
6974

7075
"Apidoc directive" should "generate markdown correctly when there is only one match" in {
@@ -116,7 +121,7 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
116121
it should "throw an exception when two matches found but javadsl/scaladsl is not in their packages" in {
117122
val thrown = the[ParadoxException] thrownBy markdown("@apidoc[ActorRef]")
118123
thrown.getMessage shouldEqual
119-
"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]."
124+
"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"
120125
}
121126

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

197+
"Inner classes" should "be linked (only scaladoc)" in {
198+
markdown("@apidoc[Consumer.Control]") shouldEqual
199+
html(
200+
"""<p><span class="group-scala">
201+
|<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">
202+
|<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>
203+
|</p>""".stripMargin
204+
)
205+
}
206+
207+
it should "be linked with a label and generics (only scaladoc)" in {
208+
markdown("@apidoc[Consumer.Control[T]](Consumer.Control)") shouldEqual
209+
html(
210+
"""<p><span class="group-scala">
211+
|<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">
212+
|<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>
213+
|</p>""".stripMargin
214+
)
215+
}
216+
217+
it should "be linked with a regex" in {
218+
markdown("@apidoc[akka.kafka.(scaladsl|javadsl).Consumer.Control]") shouldEqual
219+
html(
220+
"""<p><span class="group-scala">
221+
|<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">
222+
|<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>
223+
|</p>""".stripMargin
224+
)
225+
}
226+
227+
it should "be linked with a regex and label" in {
228+
markdown("@apidoc[Consumer.Control](akka.kafka.(scaladsl|javadsl).Consumer.Control)") shouldEqual
229+
html(
230+
"""<p><span class="group-scala">
231+
|<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">
232+
|<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>
233+
|</p>""".stripMargin
234+
)
235+
}
236+
237+
it should "generate links to inner classes" in {
238+
markdown("@apidoc[Receptionist.Command]") shouldEqual
239+
html(
240+
"""<p><span class="group-scala">
241+
|<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">
242+
|<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>
243+
|</p>""".stripMargin
244+
)
245+
}
246+
192247
"Directive with label and source" should "use the source as class pattern" in {
193248
markdown("The @apidoc[TheClass.method](Flow) { .scaladoc a=1 } thingie") shouldEqual
194249
html(

0 commit comments

Comments
 (0)