Skip to content

Commit 9d22edd

Browse files
authored
Merge pull request #3 from richardimaoka/add-implementation-richard
Add ApidocDirective implementation
2 parents 9814bc2 + 6c8cd05 commit 9d22edd

File tree

9 files changed

+417
-34
lines changed

9 files changed

+417
-34
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# sbt-paradox-unidoc
1+
# sbt-paradox-apidoc
22

33
# Maintanance notes
44

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ lazy val root = project
55
.settings(
66
sbtPlugin := true,
77
organization := "com.lightbend.paradox",
8-
name := "sbt-paradox-unidoc",
8+
name := "sbt-paradox-apidoc",
99
addSbtPlugin(Library.sbtParadox),
1010
libraryDependencies ++= Seq(
1111
Library.fastClassPathScanner,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package com.lightbend.paradox.apidoc
6+
7+
import com.lightbend.paradox.markdown.InlineDirective
8+
import org.pegdown.Printer
9+
import org.pegdown.ast.{DirectiveNode, TextNode, Visitor}
10+
11+
class ApidocDirective(allClasses: IndexedSeq[String]) extends InlineDirective("apidoc") {
12+
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
13+
if (node.label.split('[')(0).contains('.')) {
14+
val fqcn = node.label
15+
if (allClasses.contains(fqcn)) {
16+
val label = fqcn.split('.').last
17+
syntheticNode("scala", scalaLabel(label), fqcn, node).accept(visitor)
18+
syntheticNode("java", javaLabel(label), fqcn, node).accept(visitor)
19+
} else {
20+
throw new java.lang.IllegalStateException(s"fqcn not found by @apidoc[$fqcn]")
21+
}
22+
}
23+
else {
24+
renderByClassName(node.label, node, visitor, printer)
25+
}
26+
}
27+
28+
private def baseClassName(label: String) = {
29+
val labelWithoutGenerics = label.split("\\[")(0)
30+
if (labelWithoutGenerics.endsWith("$")) labelWithoutGenerics.init
31+
else labelWithoutGenerics
32+
}
33+
34+
def javaLabel(label: String): String =
35+
scalaLabel(label).replaceAll("\\[", "&lt;").replaceAll("\\]", "&gt;").replace('_', '?')
36+
37+
def scalaLabel(label: String): String =
38+
if (label.endsWith("$")) label.init
39+
else label
40+
41+
def syntheticNode(group: String, label: String, fqcn: String, node: DirectiveNode): DirectiveNode = {
42+
val syntheticSource = new DirectiveNode.Source.Direct(fqcn)
43+
val attributes = new org.pegdown.ast.DirectiveAttributes.AttributeMap()
44+
new DirectiveNode(DirectiveNode.Format.Inline, group, null, null, attributes, null,
45+
new DirectiveNode(DirectiveNode.Format.Inline, group + "doc", label, syntheticSource, node.attributes, fqcn,
46+
new TextNode(label)
47+
))
48+
}
49+
50+
def renderByClassName(label: String, node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
51+
val query = node.label.replaceAll("\\\\_", "_")
52+
val className = baseClassName(query)
53+
val classSuffix = if (query.endsWith("$")) "$" else ""
54+
55+
val matches = allClasses.filter(_.endsWith('.' + className))
56+
matches.size match {
57+
case 0 =>
58+
throw new java.lang.IllegalStateException(s"No matches found for $query")
59+
case 1 if matches(0).contains("adsl") =>
60+
throw new java.lang.IllegalStateException(s"Match for $query only found in one language: ${matches(0)}")
61+
case 1 =>
62+
syntheticNode("scala", scalaLabel(query), matches(0) + classSuffix, node).accept(visitor)
63+
syntheticNode("java", javaLabel(query), matches(0) + classSuffix, node).accept(visitor)
64+
case 2 if matches.forall(_.contains("adsl")) =>
65+
matches.foreach(m => {
66+
if (!m.contains("javadsl"))
67+
syntheticNode("scala", scalaLabel(query), m + classSuffix, node).accept(visitor)
68+
if (!m.contains("scaladsl"))
69+
syntheticNode("java", javaLabel(query), m + classSuffix, node).accept(visitor)
70+
})
71+
case n =>
72+
throw new java.lang.IllegalStateException(
73+
s"$n matches found for $query, but not javadsl/scaladsl: ${matches.mkString(", ")}. " +
74+
s"You may want to use the fully qualified class name as @apidoc[fqcn] instead of @apidoc[${label}]."
75+
)
76+
}
77+
}
78+
79+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package com.lightbend.paradox.apidoc
6+
7+
import sbt._
8+
9+
object ApidocKeys {
10+
val apidocRootPackage = settingKey[String]("")
11+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package com.lightbend.paradox.apidoc
6+
7+
import _root_.io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
8+
import com.lightbend.paradox.markdown.Writer
9+
import com.lightbend.paradox.sbt.ParadoxPlugin
10+
import com.lightbend.paradox.sbt.ParadoxPlugin.autoImport.paradoxDirectives
11+
import sbt.Keys.fullClasspath
12+
import sbt._
13+
14+
import scala.collection.JavaConverters._
15+
16+
object ApidocPlugin extends AutoPlugin {
17+
import ApidocKeys._
18+
19+
val version = ParadoxPlugin.readProperty("akka-paradox.properties", "akka.paradox.version")
20+
21+
override def requires: Plugins = ParadoxPlugin
22+
23+
override def trigger: PluginTrigger = AllRequirements
24+
25+
override def projectSettings: Seq[Setting[_]] = apidocSettings(Compile)
26+
27+
def apidocParadoxGlobalSettings: Seq[Setting[_]] = Seq(
28+
apidocRootPackage := "scala",
29+
paradoxDirectives ++= Def.taskDyn {
30+
val classpath = (fullClasspath in Compile).value.files.map(_.toURI.toURL).toArray
31+
val classLoader = new java.net.URLClassLoader(classpath, this.getClass.getClassLoader)
32+
val scanner = new FastClasspathScanner(apidocRootPackage.value).addClassLoader(classLoader).scan()
33+
val allClasses = scanner.getNamesOfAllClasses.asScala.toVector
34+
Def.task { Seq(
35+
{ _: Writer.Context new ApidocDirective(allClasses) }
36+
)}
37+
}.value
38+
)
39+
40+
def apidocSettings(config: Configuration): Seq[Setting[_]] = apidocParadoxGlobalSettings ++ inConfig(config)(Seq(
41+
// scoped settings here
42+
))
43+
}

src/main/scala/com/lightbend/paradox/unidoc/UnidocPlugin.scala

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package com.lightbend.paradox.apidoc
6+
7+
import com.lightbend.paradox.markdown.Writer
8+
9+
class ApidocPluginSpec extends MarkdownBaseSpec {
10+
val rootPackage = "akka"
11+
12+
val allClasses = Array(
13+
"akka.actor.ActorRef",
14+
"akka.actor.typed.ActorRef",
15+
"akka.cluster.client.ClusterClient",
16+
"akka.cluster.client.ClusterClient$",
17+
"akka.dispatch.Envelope",
18+
"akka.http.javadsl.model.sse.ServerSentEvent",
19+
"akka.http.javadsl.marshalling.Marshaller",
20+
"akka.http.javadsl.marshalling.Marshaller$",
21+
"akka.http.scaladsl.marshalling.Marshaller",
22+
"akka.http.scaladsl.marshalling.Marshaller$",
23+
"akka.stream.javadsl.Source",
24+
"akka.stream.javadsl.Source$",
25+
"akka.stream.scaladsl.Source",
26+
"akka.stream.scaladsl.Source$",
27+
"akka.stream.javadsl.Flow",
28+
"akka.stream.javadsl.Flow$",
29+
"akka.stream.scaladsl.Flow",
30+
"akka.stream.scaladsl.Flow$"
31+
)
32+
33+
override val markdownWriter = new Writer(
34+
linkRenderer = Writer.defaultLinks,
35+
verbatimSerializers = Writer.defaultVerbatims,
36+
serializerPlugins = Writer.defaultPlugins(
37+
Writer.defaultDirectives ++ Seq(
38+
(_: Writer.Context) => new ApidocDirective(allClasses)
39+
)
40+
)
41+
)
42+
43+
implicit val context = writerContextWithProperties(
44+
"scaladoc.akka.base_url" -> "https://doc.akka.io/api/akka/2.5",
45+
"scaladoc.akka.http.base_url" -> "https://doc.akka.io/api/akka-http/current",
46+
"javadoc.akka.base_url" -> "https://doc.akka.io/japi/akka/2.5",
47+
"javadoc.akka.http.base_url" -> "https://doc.akka.io/japi/akka-http/current",
48+
)
49+
50+
"Apidoc directive" should "generate markdown correctly when there is only one match" in {
51+
markdown("@apidoc[Envelope]") shouldEqual
52+
html(
53+
"""<p><span class="group-scala">
54+
|<a href="https://doc.akka.io/api/akka/2.5/akka/dispatch/Envelope.html">Envelope</a></span><span class="group-java">
55+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/dispatch/Envelope.html">Envelope</a></span>
56+
|</p>""".stripMargin
57+
)
58+
}
59+
60+
it should "throw an exception when there is no match" in {
61+
val thrown = the[IllegalStateException] thrownBy markdown("@apidoc[ThereIsNoSuchClass]")
62+
thrown.getMessage shouldEqual
63+
"No matches found for ThereIsNoSuchClass"
64+
}
65+
66+
67+
it should "generate markdown correctly when 2 matches found and their package names include javadsl/scaladsl" in {
68+
markdown("@apidoc[Flow]") shouldEqual
69+
html(
70+
"""<p><span class="group-java">
71+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Flow.html">Flow</a></span><span class="group-scala">
72+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Flow.html">Flow</a></span>
73+
|</p>""".stripMargin
74+
)
75+
}
76+
77+
it should "throw an exception when two matches found but javadsl/scaladsl is not in their packages" in {
78+
val thrown = the[IllegalStateException] thrownBy markdown("@apidoc[ActorRef]")
79+
thrown.getMessage shouldEqual
80+
"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]."
81+
}
82+
83+
it should "generate markdown correctly when fully qualified class name (fqcn) is specified as @apidoc[fqcn]" in {
84+
markdown("@apidoc[akka.actor.ActorRef]") shouldEqual
85+
html(
86+
"""<p><span class="group-scala">
87+
|<a href="https://doc.akka.io/api/akka/2.5/akka/actor/ActorRef.html">ActorRef</a></span><span class="group-java">
88+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/actor/ActorRef.html">ActorRef</a></span>
89+
|</p>""".stripMargin
90+
)
91+
}
92+
93+
it should "throw an exception when `.` is in the [label], but the label is not fqcn" in {
94+
val thrown = the[IllegalStateException] thrownBy markdown("@apidoc[actor.typed.ActorRef]")
95+
thrown.getMessage shouldEqual
96+
"fqcn not found by @apidoc[actor.typed.ActorRef]"
97+
}
98+
99+
it should "generate markdown correctly for a companion object" in {
100+
markdown("@apidoc[ClusterClient$]") shouldEqual
101+
html(
102+
"""<p><span class="group-scala">
103+
|<a href="https://doc.akka.io/api/akka/2.5/akka/cluster/client/ClusterClient$.html">ClusterClient</a></span><span class="group-java">
104+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/cluster/client/ClusterClient$.html">ClusterClient</a></span>
105+
|</p>""".stripMargin
106+
)
107+
}
108+
109+
it should "generate markdown correctly for type parameter and wildcard" in {
110+
markdown("@apidoc[Source[ServerSentEvent, \\_]]") shouldEqual
111+
html(
112+
"""<p><span class="group-java">
113+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Source.html">Source&lt;ServerSentEvent, ?&gt;</a></span><span class="group-scala">
114+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Source.html">Source[ServerSentEvent, _]</a></span>
115+
|</p>""".stripMargin
116+
)
117+
}
118+
119+
it should "generate markdown correctly for type parameters with concrete names" in {
120+
markdown("@apidoc[Flow[Message, Message, Mat]]") shouldEqual
121+
html(
122+
"""<p><span class="group-java">
123+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Flow.html">Flow&lt;Message, Message, Mat&gt;</a></span><span class="group-scala">
124+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Flow.html">Flow[Message, Message, Mat]</a></span>
125+
|</p>""".stripMargin
126+
)
127+
}
128+
129+
it should "generate markdown correctly for nested type parameters" in {
130+
markdown("@apidoc[Marshaller[Try[A], B]]") shouldEqual
131+
html(
132+
"""<p><span class="group-java">
133+
|<a href="https://doc.akka.io/japi/akka-http/current/?akka/http/javadsl/marshalling/Marshaller.html">Marshaller&lt;Try&lt;A&gt;, B&gt;</a></span><span class="group-scala">
134+
|<a href="https://doc.akka.io/api/akka-http/current/akka/http/scaladsl/marshalling/Marshaller.html">Marshaller[Try[A], B]</a></span>
135+
|</p>""".stripMargin
136+
)
137+
}
138+
}

0 commit comments

Comments
 (0)