diff --git a/src/main/scala-2/com/github/swagger/scala/converter/ErasureHelper.scala b/src/main/scala-2/com/github/swagger/scala/converter/ErasureHelper.scala index 350cc16..a6196bd 100644 --- a/src/main/scala-2/com/github/swagger/scala/converter/ErasureHelper.scala +++ b/src/main/scala-2/com/github/swagger/scala/converter/ErasureHelper.scala @@ -1,6 +1,7 @@ package com.github.swagger.scala.converter import scala.reflect.runtime.universe +import scala.util.Try object ErasureHelper { @@ -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 => diff --git a/src/main/scala/com/github/swagger/scala/converter/SwaggerScalaModelConverter.scala b/src/main/scala/com/github/swagger/scala/converter/SwaggerScalaModelConverter.scala index e8e0f8f..77ebce4 100644 --- a/src/main/scala/com/github/swagger/scala/converter/SwaggerScalaModelConverter.scala +++ b/src/main/scala/com/github/swagger/scala/converter/SwaggerScalaModelConverter.scala @@ -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} @@ -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] @@ -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 @@ -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) { @@ -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 } @@ -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) } } } diff --git a/src/test/scala/com/github/swagger/scala/converter/ModelPropertyParserTest.scala b/src/test/scala/com/github/swagger/scala/converter/ModelPropertyParserTest.scala index 56fad8d..b92cb9f 100644 --- a/src/test/scala/com/github/swagger/scala/converter/ModelPropertyParserTest.scala +++ b/src/test/scala/com/github/swagger/scala/converter/ModelPropertyParserTest.scala @@ -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 @@ -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 { @@ -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 diff --git a/src/test/scala/models/ModelWOptionInt.scala b/src/test/scala/models/ModelWOptionInt.scala index ee3a6c0..7f23ee2 100644 --- a/src/test/scala/models/ModelWOptionInt.scala +++ b/src/test/scala/models/ModelWOptionInt.scala @@ -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]) } diff --git a/src/test/scala/models/ModelWOptionString.scala b/src/test/scala/models/ModelWOptionString.scala index 3d4ec92..6a80a98 100644 --- a/src/test/scala/models/ModelWOptionString.scala +++ b/src/test/scala/models/ModelWOptionString.scala @@ -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])