Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Generate Description from Scaladoc #499

Merged
merged 5 commits into from
Sep 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,51 @@ summary: Top Page
It can be configured in `build.sbt`.
This setting allows you to set the `${controllerName}.${methodName}` to name the operationId.

```
```sbt
swaggerOperationIdNamingFully := true
```

#### Need a schema description?

Using [runtime-scaladoc-reader](https://github.com/takezoe/runtime-scaladoc-reader), a description can be generated from Scaladoc comments written in the case class.

⚠️ Schema generation from documentation comments is very useful, but **should never be used** if the scope of scaladoc documentation is different from the scope of OpenAPI documentation.

Add the required dependencies and Compiler Plugin to `build.sbt` and configure it for use.

```sbt
embedScaladoc := true
addCompilerPlugin("com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.3")
libraryDependencies += "com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.3"
```

For example, a case class might be written as follows.

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

The generated JSON will look like this.

```json
{
"DayOfWeek": {
"properties": {
"name": {
"type": "string",
"description": "e.g. Sunday, Monday, TuesDay..."
}
},
"required": [
"name"
]
}
}
```

#### Is play java supported?

you can generate models definition from java POJO by setting the flag:
Expand Down
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,22 +1,31 @@
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(
modelQualifier: DomainModelQualifier = PrefixDomainModelQualifier(),
mappings: CustomMappings = Nil,
swaggerPlayJava: Boolean = false,
_mapper: ObjectMapper = new ObjectMapper(),
namingStrategy: NamingStrategy = NamingStrategy.None
namingStrategy: NamingStrategy = NamingStrategy.None,
embedScaladoc: Boolean = false
)(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 +40,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 +71,33 @@ final case class DefinitionGenerator(
case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
}.toList.flatMap(_.paramLists).headOption.getOrElse(Nil)

fields.map { field ⇒
val paramDescriptions = if (embedScaladoc) {
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

(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
} else {
Map.empty[String, String]
}

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 +108,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 Expand Up @@ -142,11 +199,13 @@ object DefinitionGenerator {
def apply(
domainNameSpace: String,
customParameterTypeMappings: CustomMappings,
namingStrategy: NamingStrategy
namingStrategy: NamingStrategy,
embedScaladoc: Boolean
)(implicit cl: ClassLoader): DefinitionGenerator =
DefinitionGenerator(
PrefixDomainModelQualifier(domainNameSpace),
customParameterTypeMappings,
namingStrategy = namingStrategy
namingStrategy = namingStrategy,
embedScaladoc = embedScaladoc
)
}
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
Loading