All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.iheart.playSwagger.generator.DefinitionGenerator.scala Maven / Gradle / Ivy

The newest version!
package com.iheart.playSwagger.generator

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.ParametricType
import com.iheart.playSwagger.domain.Definition
import com.iheart.playSwagger.domain.parameter.{GenSwaggerParameter, SwaggerParameter}
import net.steppschuh.markdowngenerator.MarkdownElement
import net.steppschuh.markdowngenerator.link.Link
import net.steppschuh.markdowngenerator.table.Table
import net.steppschuh.markdowngenerator.text.Text
import net.steppschuh.markdowngenerator.text.code.{Code, CodeBlock}
import net.steppschuh.markdowngenerator.text.heading.Heading
import play.routes.compiler.Parameter

final case class DefinitionGenerator(
    mapper: SwaggerParameterMapper,
    swaggerPlayJava: Boolean = false,
    _mapper: ObjectMapper = new ObjectMapper(),
    namingConvention: NamingConvention = NamingConvention.None,
    embedScaladoc: Boolean = false
)(implicit cl: ClassLoader) {

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

  private def dealiasParams(t: Type): Type = {
    t.toString match {
      case refinedTypePattern(_) => t.typeArgs.headOption.getOrElse(t)
      case _ =>
        appliedType(
          t.dealias.typeConstructor,
          t.typeArgs.map { arg =>
            dealiasParams(arg.dealias)
          }
        )
    }
  }

  private def scalaDocToMarkdown: PartialFunction[iScaladoc.Term, MarkdownElement] = {
    case value: iScaladoc.Text =>
      new Text(value.parts.map {
        case word: iScaladoc.Word => new Text(word.value)
        case link: iScaladoc.Link => new Link(link.anchor.mkString(" "), link.ref)
        case code: iScaladoc.CodeExpr => new Code(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) {
        definitionForPOJO(tpe)
      } else {
        val fields = tpe.decls.collectFirst {
          case m: MethodSymbol if m.isPrimaryConstructor => m
        }.toList.flatMap(_.paramLists).headOption.getOrElse(Nil)

        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 = namingConvention(field.name.decodedName.toString)

          val rawTypeName = dealiasParams(field.typeSignature).toString match {
            case refinedTypePattern(_) => field.info.dealias.typeArgs.head.toString
            case v => v
          }
          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)
          mapper.mapParam(param, paramDescriptions.get(field.name.decodedName.toString))
        }
      }

      Definition(
        name = reifiedTypeName,
        properties = properties
      )
  }

  private def definitionForPOJO(tpe: Type): Seq[SwaggerParameter] = {
    val m = runtimeMirror(cl)
    val clazz = m.runtimeClass(tpe.typeSymbol.asClass)
    val `type` = _mapper.constructType(clazz)
    val beanDesc: BeanDescription = _mapper.getSerializationConfig.introspect(`type`)
    val beanProperties = beanDesc.findProperties
    val ignoreProperties = beanDesc.getIgnoredPropertyNames
    val propertySet = JavaConverters.asScalaIteratorConverter(beanProperties.iterator()).asScala.toSeq
    propertySet.filter(bd => !ignoreProperties.contains(bd.getName)).map { entry =>
      val name = entry.getName
      val className = entry.getPrimaryMember.getType.getRawClass.getName
      val generalTypeName = if (entry.getField != null && entry.getField.getType.hasGenericTypes) {
        val generalType = entry.getField.getType.getContentType.getRawClass.getName
        s"$className[$generalType]"
      } else {
        className
      }
      val typeName = if (!entry.isRequired) {
        s"Option[$generalTypeName]"
      } else {
        generalTypeName
      }
      val param = Parameter(name, typeName, None, None)
      mapper.mapParam(param, None)
    }
  }

  def definition[T: TypeTag]: Definition = definition(ParametricType[T])

  def definition(className: String): Definition = definition(ParametricType(className))

  def allDefinitions(typeNames: Seq[String]): List[Definition] = {
    def genSwaggerParameter: PartialFunction[SwaggerParameter, GenSwaggerParameter] = {
      case p: GenSwaggerParameter => p
    }

    def allReferredDefs(defName: String, memo: List[Definition]): List[Definition] = {
      def findRefTypes(p: GenSwaggerParameter): Seq[String] =
        p.referenceType.toSeq ++ {
          p.items.toSeq.collect(genSwaggerParameter).flatMap(findRefTypes)
        }

      memo.find(_.name == defName) match {
        case Some(_) => memo
        case None =>
          val thisDef = definition(defName)
          val refNames: Seq[String] = for {
            p <- thisDef.properties.collect(genSwaggerParameter)
            className <- findRefTypes(p)
            if mapper.isReference(className)
          } yield className

          refNames.foldLeft(thisDef :: memo) { (foundDefs, refName) =>
            allReferredDefs(refName, foundDefs)
          }
      }
    }

    typeNames.foldLeft(List.empty[Definition]) { (memo, typeName) =>
      allReferredDefs(typeName, memo)
    }
  }
}

object DefinitionGenerator {
  def apply(
      mapper: SwaggerParameterMapper,
      swaggerPlayJava: Boolean,
      namingConvention: NamingConvention
  )(implicit cl: ClassLoader): DefinitionGenerator =
    new DefinitionGenerator(
      mapper,
      swaggerPlayJava,
      namingConvention = namingConvention
    )

  def apply(
      mapper: SwaggerParameterMapper,
      namingConvention: NamingConvention,
      embedScaladoc: Boolean
  )(implicit cl: ClassLoader): DefinitionGenerator =
    new DefinitionGenerator(
      mapper = mapper,
      namingConvention = namingConvention,
      embedScaladoc = embedScaladoc
    )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy