Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.swagger.scala.converter

import scala.reflect.runtime.universe
import scala.util.Try

object ErasureHelper {

Expand All @@ -10,14 +11,13 @@ object ErasureHelper {
val moduleSymbol = mirror.moduleSymbol(Class.forName(cls.getName))
val ConstructorName = "apply"
val companion: universe.Symbol = moduleSymbol.typeSignature.member(universe.TermName(ConstructorName))
val properties = if (companion.fullName.endsWith(ConstructorName)) {
companion.asMethod.paramLists.flatten
} else {
val sym = mirror.staticClass(cls.getName)
sym.selfType.members
.filterNot(_.isMethod)
.filterNot(_.isClass)
}
val properties =
Try(companion.asTerm.alternatives.head.asMethod.paramLists.flatten).getOrElse {
val sym = mirror.staticClass(cls.getName)
sym.selfType.members
.filterNot(_.isMethod)
.filterNot(_.isClass)
}

properties.flatMap { prop: universe.Symbol =>
val maybeClass: Option[Class[_]] = prop.typeSignature.typeArgs.headOption.flatMap { signature =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package com.github.swagger.scala.converter

import java.lang.annotation.Annotation
import java.lang.reflect.ParameterizedType
import java.util.Iterator
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.{JavaType, ObjectMapper}
import com.fasterxml.jackson.databind.`type`.ReferenceType
import com.fasterxml.jackson.module.scala.introspect.{BeanIntrospector, PropertyDescriptor}
import com.fasterxml.jackson.module.scala.{DefaultScalaModule, JsonScalaEnumeration}
Expand All @@ -22,11 +21,10 @@ import scala.util.control.NonFatal
class AnnotatedTypeForOption extends AnnotatedType

object SwaggerScalaModelConverter {
val objectMapper = Json.mapper().registerModule(DefaultScalaModule)
val objectMapper: ObjectMapper = Json.mapper().registerModule(DefaultScalaModule)
}

class SwaggerScalaModelConverter extends ModelResolver(SwaggerScalaModelConverter.objectMapper) {
SwaggerScalaModelConverter

private val logger = LoggerFactory.getLogger(classOf[SwaggerScalaModelConverter])
private val VoidClass = classOf[Void]
Expand All @@ -40,7 +38,7 @@ class SwaggerScalaModelConverter extends ModelResolver(SwaggerScalaModelConverte
private val ProductClass = classOf[Product]
private val AnyClass = classOf[Any]

override def resolve(`type`: AnnotatedType, context: ModelConverterContext, chain: Iterator[ModelConverter]): Schema[_] = {
override def resolve(`type`: AnnotatedType, context: ModelConverterContext, chain: util.Iterator[ModelConverter]): Schema[_] = {
val javaType = _mapper.constructType(`type`.getType)
val cls = javaType.getRawClass

Expand Down Expand Up @@ -80,13 +78,12 @@ class SwaggerScalaModelConverter extends ModelResolver(SwaggerScalaModelConverte
val introspector = BeanIntrospector(cls)
val erasedProperties = ErasureHelper.erasedOptionalPrimitives(cls)
introspector.properties.foreach { property =>
val propertyClass = getPropertyClass(property)
val (propertyClass, propertyAnnotations) = getPropertyClassAndAnnotations(property)
val isOptional = isOption(propertyClass)
val propertyAnnotations = getPropertyAnnotations(property)
val schemaOverrideClass = propertyAnnotations.collectFirst {
case s: SchemaAnnotation if s.implementation() != VoidClass => s.implementation()
}
if (schemaOverrideClass.isEmpty) {
if (schemaOverrideClass.isEmpty && schema.getProperties != null) {
erasedProperties.get(property.name).foreach { erasedType =>
val primitiveType = PrimitiveType.fromType(erasedType)
if (primitiveType != null && isOptional) {
Expand Down Expand Up @@ -137,7 +134,9 @@ class SwaggerScalaModelConverter extends ModelResolver(SwaggerScalaModelConverte
val propAsString = objectMapper.writeValueAsString(itemSchema)
val correctedSchema = objectMapper.readValue(propAsString, primitiveProperty.getClass)
correctedSchema.setType(primitiveProperty.getType)
correctedSchema.setFormat(primitiveProperty.getFormat)
if (itemSchema.getFormat == null) {
correctedSchema.setFormat(primitiveProperty.getFormat)
}
correctedSchema
}

Expand Down Expand Up @@ -261,54 +260,30 @@ class SwaggerScalaModelConverter extends ModelResolver(SwaggerScalaModelConverte
}
}

private def getPropertyClass(property: PropertyDescriptor): Class[_] = {
private def getPropertyClassAndAnnotations(property: PropertyDescriptor): (Class[_], Seq[Annotation]) = {
property.param match {
case Some(constructorParameter) => {
case Some(constructorParameter) =>
val types = constructorParameter.constructor.getParameterTypes
if (constructorParameter.index > types.size) {
AnyClass
val annotations = constructorParameter.constructor.getParameterAnnotations
val index = constructorParameter.index
if (index > types.size) {
(AnyClass, Seq.empty)
} else {
types(constructorParameter.index)
val onlyType = types(index)
val onlyAnnotations = if (index > annotations.size) { Seq.empty[Annotation] } else { annotations(index).toIndexedSeq }
(onlyType, onlyAnnotations)
}
}
case _ => property.field match {
case Some(field) => field.getType
case _ => property.setter match {
case Some(setter) if setter.getParameterCount == 1 => {
setter.getParameterTypes()(0)
}
case _ => property.beanSetter match {
case Some(setter) if setter.getParameterCount == 1 => {
setter.getParameterTypes()(0)
}
case _ => AnyClass
}
}
}
}
}

private def getPropertyAnnotations(property: PropertyDescriptor): Seq[Annotation] = {
property.param match {
case Some(constructorParameter) => {
val types = constructorParameter.constructor.getParameterAnnotations
if (constructorParameter.index > types.size) {
Seq.empty
} else {
types(constructorParameter.index).toSeq
}
}
case _ => property.field match {
case Some(field) => field.getAnnotations.toSeq
case Some(field) => (field.getType, field.getAnnotations.toSeq)
case _ => property.setter match {
case Some(setter) if setter.getParameterCount == 1 => {
setter.getAnnotations().toSeq
(setter.getParameterTypes()(0), setter.getAnnotations.toSeq)
}
case _ => property.beanSetter match {
case Some(setter) if setter.getParameterCount == 1 => {
setter.getAnnotations().toSeq
(setter.getParameterTypes()(0), setter.getAnnotations.toSeq)
}
case _ => Seq.empty
case _ => (AnyClass, Seq.empty)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.github.swagger.scala.converter
import io.swagger.v3.core.converter._
import io.swagger.v3.core.util.Json
import io.swagger.v3.oas.models.media._
import models.NestingObject.NestedModelWOptionInt
import models.NestingObject.{NestedModelWOptionInt, NoProperties}
import models._
import org.scalatest.OptionValues
import org.scalatest.flatspec.AnyFlatSpec
Expand Down Expand Up @@ -38,6 +38,13 @@ class ModelPropertyParserTest extends AnyFlatSpec with Matchers with OptionValue
stringWithDataType should not be (null)
stringWithDataType shouldBe a [StringSchema]
nullSafeList(stringWithDataType.getRequired) shouldBe empty

val ipAddress = model.value.getProperties().get("ipAddress")
ipAddress should not be (null)
ipAddress shouldBe a[StringSchema]
ipAddress.getDescription shouldBe "An IP address"
ipAddress.getFormat shouldBe "IPv4 or IPv6"
nullSafeList(ipAddress.getRequired) shouldBe empty
}

it should "process Option[Model] as Model" in {
Expand Down Expand Up @@ -135,6 +142,16 @@ class ModelPropertyParserTest extends AnyFlatSpec with Matchers with OptionValue
nullSafeList(model.value.getRequired) shouldBe empty
}

it should "process Model without any properties" in {
val converter = ModelConverters.getInstance()
val schemas = converter.readAll(classOf[NoProperties]).asScala.toMap
val model = schemas.get("NoProperties")
model should be(defined)
model.value.getProperties should be (null)
model.get shouldBe a[Schema[_]]
model.get.getDescription shouldBe "An empty case class"
}

it should "process Model with nested Scala Option Int with Schema Override" in {
val converter = ModelConverters.getInstance()
val schemas = converter.readAll(classOf[ModelWOptionIntSchemaOverride]).asScala.toMap
Expand Down
11 changes: 11 additions & 0 deletions src/test/scala/models/ModelWOptionInt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ case class ModelWOptionInt(optInt: Option[Int])

object NestingObject {
case class NestedModelWOptionInt(optInt: Option[Int])

@Schema(description = "An empty case class")
case class NoProperties()

object NestedModelWOptionInt {

def apply(nonOptional: Int): NestedModelWOptionInt = {
NestedModelWOptionInt(Some(nonOptional))
}
}

case class NestedModelWOptionIntSchemaOverride(@Schema(description = "This is an optional int") optInt: Option[Int])
}

Expand Down
9 changes: 5 additions & 4 deletions src/test/scala/models/ModelWOptionString.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package models
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Schema

case class ModelWOptionString (
stringOpt: Option[String],
stringWithDataTypeOpt: Option[String])

case class ModelWOptionString (stringOpt: Option[String],
stringWithDataTypeOpt: Option[String],
@Schema(description = "An IP address", format = "IPv4 or IPv6")
ipAddress: Option[String]
)

case class ModelWOptionModel (modelOpt: Option[ModelWOptionString])

Expand Down