Skip to content

Commit

Permalink
feat: Generate Description from Scaladoc
Browse files Browse the repository at this point in the history
  • Loading branch information
Javakky-pxv committed Sep 6, 2022
1 parent 888715a commit 8ece38a
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 12 deletions.
7 changes: 6 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ lazy val playSwagger = project.in(file("core"))
Dependencies.enumeratum ++
Dependencies.refined ++
Dependencies.test ++
Dependencies.yaml,
Dependencies.yaml ++ Seq(
"com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.3",
"org.scalameta" %% "scalameta" % "4.5.13",
"net.steppschuh.markdowngenerator" % "markdowngenerator" % "1.3.1.1"
),
addCompilerPlugin("com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.3"),
scalaVersion := scalaV,
crossScalaVersions := Seq(scalaVersion.value, "2.13.8"),
scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package com.iheart.playSwagger

import scala.collection.JavaConverters
import scala.meta.internal.parsers.ScaladocParser
import scala.meta.internal.{Scaladoc => iScaladoc}
import scala.reflect.runtime.universe._

import com.fasterxml.jackson.databind.{BeanDescription, ObjectMapper}
import com.github.takezoe.scaladoc.Scaladoc
import com.iheart.playSwagger.Domain.{CustomMappings, Definition, GenSwaggerParameter, SwaggerParameter}
import com.iheart.playSwagger.SwaggerParameterMapper.mapParam
import net.steppschuh.markdowngenerator.MarkdownElement
import net.steppschuh.markdowngenerator.table.Table
import net.steppschuh.markdowngenerator.text.Text
import net.steppschuh.markdowngenerator.text.code.CodeBlock
import net.steppschuh.markdowngenerator.text.heading.Heading
import play.routes.compiler.Parameter

final case class DefinitionGenerator(
Expand All @@ -16,7 +24,7 @@ final case class DefinitionGenerator(
namingStrategy: NamingStrategy = NamingStrategy.None
)(implicit cl: ClassLoader) {

private val refinedTypePattern = raw"(eu\.timepit\.refined\.api\.Refined(?:\[.+\])?)".r
private val refinedTypePattern = raw"(eu\.timepit\.refined\.api\.Refined(?:\[.+])?)".r

def dealiasParams(t: Type): Type = {
t.toString match {
Expand All @@ -31,6 +39,28 @@ final case class DefinitionGenerator(
}
}

private def scalaDocToMarkdown: PartialFunction[iScaladoc.Term, MarkdownElement] = {
case value: iScaladoc.Text =>
new Text(value.parts.map {
case word: iScaladoc.Word => word.value
case link: iScaladoc.Link => s"[${link.anchor.mkString(" ")}](${link.ref})}"
case code: iScaladoc.CodeExpr => s"`${code.code}`"
}.mkString(" "))
case code: iScaladoc.CodeBlock => new CodeBlock(code, "scala")
case code: iScaladoc.MdCodeBlock =>
new CodeBlock(code.code.mkString("\n"), code.info.mkString(":"))
case head: iScaladoc.Heading => new Heading(head, 1)
case table: iScaladoc.Table =>
val builder = new Table.Builder().withAlignments(Table.ALIGN_RIGHT, Table.ALIGN_LEFT).addRow(
table.header.cols: _*
)
table.rows.foreach(row => builder.addRow(row.cols: _*))
builder.build()
// TODO: Support List
// https://github.com/Steppschuh/Java-Markdown-Generator/pull/13
case _ => new Text("")
}

def definition: ParametricType Definition = {
case parametricType @ ParametricType(tpe, reifiedTypeName, _, _)
val properties = if (swaggerPlayJava) {
Expand All @@ -40,7 +70,29 @@ final case class DefinitionGenerator(
case m: MethodSymbol if m.isPrimaryConstructor m
}.toList.flatMap(_.paramLists).headOption.getOrElse(Nil)

fields.map { field
val scaladoc = for {
annotation <- tpe.typeSymbol.annotations
if typeOf[Scaladoc] == annotation.tree.tpe
value <- annotation.tree.children.tail.headOption
docTree <- value.children.tail.headOption
docString = docTree.toString().tail.init.replace("\\n", "\n")
doc <- ScaladocParser.parse(docString)
} yield doc

val paramDescriptions = (for {
doc <- scaladoc
paragraph <- doc.para
term <- paragraph.terms
tag <- term match {
case iScaladoc.Tag(iScaladoc.TagType.Param, Some(iScaladoc.Word(key)), Seq(text)) =>
Some(key -> text)
case _ => None
}
} yield tag).map {
case (name, term) => name -> scalaDocToMarkdown(term).toString
}.toMap

fields.map { field: Symbol
// TODO: find a better way to get the string representation of typeSignature
val name = namingStrategy(field.name.decodedName.toString)

Expand All @@ -51,7 +103,7 @@ final case class DefinitionGenerator(
val typeName = parametricType.resolve(rawTypeName)
// passing None for 'fixed' and 'default' here, since we're not dealing with route parameters
val param = Parameter(name, typeName, None, None)
mapParam(param, modelQualifier, mappings)
mapParam(param, modelQualifier, mappings, paramDescriptions.get(field.name.decodedName.toString))
}
}

Expand Down
7 changes: 5 additions & 2 deletions core/src/main/scala/com/iheart/playSwagger/Domain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ object Domain {
def required: Boolean
def nullable: Option[Boolean]
def default: Option[JsValue]
def description: Option[String]

def update(required: Boolean, nullable: Boolean, default: Option[JsValue]): SwaggerParameter
}
Expand All @@ -31,7 +32,8 @@ object Domain {
default: Option[JsValue] = None,
example: Option[JsValue] = None,
items: Option[SwaggerParameter] = None,
enum: Option[Seq[String]] = None
enum: Option[Seq[String]] = None,
description: Option[String] = None
) extends SwaggerParameter {
def update(_required: Boolean, _nullable: Boolean, _default: Option[JsValue]): GenSwaggerParameter =
copy(required = _required, nullable = Some(_nullable), default = _default)
Expand All @@ -43,7 +45,8 @@ object Domain {
specAsProperty: Option[JsObject],
required: Boolean = true,
nullable: Option[Boolean] = None,
default: Option[JsValue] = None
default: Option[JsValue] = None,
description: Option[String] = None
) extends SwaggerParameter {
def update(_required: Boolean, _nullable: Boolean, _default: Option[JsValue]): CustomSwaggerParameter =
copy(required = _required, nullable = Some(_nullable), default = _default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ object SwaggerParameterMapper {
def mapParam(
parameter: Parameter,
modelQualifier: DomainModelQualifier = PrefixDomainModelQualifier(),
customMappings: CustomMappings = Nil
customMappings: CustomMappings = Nil,
description: Option[String] = None
)(implicit cl: ClassLoader): SwaggerParameter = {

def removeKnownPrefixes(name: String) =
Expand Down Expand Up @@ -57,14 +58,15 @@ object SwaggerParameterMapper {
tp: String,
format: Option[String] = None,
enum: Option[Seq[String]] = None
) =
): GenSwaggerParameter =
GenSwaggerParameter(
parameter.name,
`type` = Some(tp),
format = format,
required = defaultValueO.isEmpty,
default = defaultValueO,
enum = enum
enum = enum,
description = description
)

val enumParamMF: MappingFunction = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ final case class SwaggerSpecGenerator(
(under \ 'default).writeNullable[JsValue] ~
(under \ 'example).writeNullable[JsValue] ~
(under \ "items").writeNullable[SwaggerParameter](propWrites) ~
(under \ "enum").writeNullable[Seq[String]]
(under \ "enum").writeNullable[Seq[String]] ~
(__ \ "description").writeNullable[String]
)(unlift(GenSwaggerParameter.unapply))
}

Expand Down Expand Up @@ -330,9 +331,20 @@ final case class SwaggerSpecGenerator(
(__ \ 'example).writeNullable[JsValue] ~
(__ \ "$ref").writeNullable[String] ~
(__ \ "items").lazyWriteNullable[SwaggerParameter](propWrites) ~
(__ \ "enum").writeNullable[Seq[String]]
(__ \ "enum").writeNullable[Seq[String]] ~
(__ \ "description").writeNullable[String]
)(p
(p.`type`, p.format, p.nullable, p.default, p.example, p.referenceType.map(referencePrefix + _), p.items, p.enum)
(
p.`type`,
p.format,
p.nullable,
p.default,
p.example,
p.referenceType.map(referencePrefix + _),
p.items,
p.enum,
p.description
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ case class Keeper(internalFieldName1: String, internalFieldName2: Int)

case class Subject(name: String)

/**
* @param name e.g. Sunday, Monday, TuesDay...
*/
case class DayOfWeek(name: String)

case class PolymorphicContainer(item: PolymorphicItem)
Expand Down Expand Up @@ -509,6 +512,11 @@ class SwaggerSpecGeneratorIntegrationSpec extends Specification {
(dayOfWeekJson.get \ "properties" \ "name" \ "type").as[String] === "string"
}

"embedded scaladoc strings" >> {
dayOfWeekJson must beSome[JsObject]
(dayOfWeekJson.get \ "properties" \ "name" \ "description").as[String] === "e.g. Sunday, Monday, TuesDay..."
}

"parse mixin referenced external file" >> {
lazy val subjectJson = (pathJson \ "/api/subjects/dow/{subject}" \ "get").as[JsObject]

Expand Down

0 comments on commit 8ece38a

Please sign in to comment.