From 0d27e3caff760a2ed5a53958d4663c1a6a9152e3 Mon Sep 17 00:00:00 2001 From: Jan Strnad Date: Sat, 10 Aug 2019 09:28:38 +0200 Subject: [PATCH] Generate nested definition classes into companion objects. --- .../twilio/guardrail/ProtocolGenerator.scala | 103 +++++-- .../com/twilio/guardrail/SwaggerUtil.scala | 86 ++++-- .../guardrail/generators/JavaGenerator.scala | 12 +- .../guardrail/generators/ScalaGenerator.scala | 29 +- .../twilio/guardrail/terms/ScalaTerm.scala | 11 +- .../twilio/guardrail/terms/ScalaTerms.scala | 12 +- .../src/test/scala/tests/core/TypesTest.scala | 266 ++++++++++++++---- 7 files changed, 412 insertions(+), 107 deletions(-) diff --git a/modules/codegen/src/main/scala/com/twilio/guardrail/ProtocolGenerator.scala b/modules/codegen/src/main/scala/com/twilio/guardrail/ProtocolGenerator.scala index 0f676181df..b136e51483 100644 --- a/modules/codegen/src/main/scala/com/twilio/guardrail/ProtocolGenerator.scala +++ b/modules/codegen/src/main/scala/com/twilio/guardrail/ProtocolGenerator.scala @@ -2,10 +2,13 @@ package com.twilio.guardrail import _root_.io.swagger.v3.oas.models._ import _root_.io.swagger.v3.oas.models.media._ +import cats.data.NonEmptyList import cats.free.Free import cats.implicits._ +import com.twilio.guardrail.SwaggerUtil.Resolved import com.twilio.guardrail.extract.VendorExtension.VendorExtensible._ import com.twilio.guardrail.generators.RawParameterType +import com.twilio.guardrail.generators.syntax._ import com.twilio.guardrail.languages.LA import com.twilio.guardrail.protocol.terms.protocol._ import com.twilio.guardrail.terms.framework.FrameworkTerms @@ -264,33 +267,95 @@ object ProtocolGenerator { } yield supper } - private[this] def fromModel[L <: LA, F[_]](clsName: String, model: Schema[_], parents: List[SuperClass[L]], concreteTypes: List[PropMeta[L]])( + private[this] def fromModel[L <: LA, F[_]](clsName: NonEmptyList[String], + model: Schema[_], + parents: List[SuperClass[L]], + concreteTypes: List[PropMeta[L]], + definitions: List[(String, Schema[_])])( implicit M: ModelProtocolTerms[L, F], F: FrameworkTerms[L, F], + P: PolyProtocolTerms[L, F], Sc: ScalaTerms[L, F], Sw: SwaggerTerms[L, F] - ): Free[F, Either[String, ProtocolElems[L]]] = { + ): Free[F, Either[String, ClassDefinition[L]]] = { import M._ import Sc._ + for { + props <- extractProperties(model) + needCamelSnakeConversion = props.forall { case (k, _) => couldBeSnakeCase(k) } + (params, nestedClassDefinitions) <- prepareProperties(clsName, model, concreteTypes, definitions) + defn <- renderDTOClass(clsName.last, params, parents) + encoder <- encodeModel(clsName.last, needCamelSnakeConversion, params, parents) + decoder <- decodeModel(clsName.last, needCamelSnakeConversion, params, parents) + tpe <- parseTypeName(clsName.last) + staticDefns <- renderDTOStaticDefns(clsName.last, List.empty, encoder, decoder) + result <- if (parents.isEmpty && props.isEmpty) Free.pure[F, Either[String, ClassDefinition[L]]](Left("Entity isn't model")) + else { + val nestedClasses = nestedClassDefinitions.flatTraverse { classDefinition => + for { + widenClass <- widenClassDefinition(classDefinition.cls) + companionTerm <- pureTermName(classDefinition.name) + companionDefinition <- wrapToObject(companionTerm, classDefinition.staticDefns.extraImports, classDefinition.staticDefns.definitions) + widenCompanion <- widenObjectDefinition(companionDefinition) + } yield List(widenClass, widenCompanion) + } + nestedClasses.map { v => + val finalStaticDefns = staticDefns.copy(definitions = staticDefns.definitions ++ v) + tpe.toRight("Empty entity name").map(ClassDefinition[L](clsName.last, _, defn, finalStaticDefns, parents)) + } + } + } yield result + } + + private def prepareProperties[L <: LA, F[_]]( + clsName: NonEmptyList[String], + model: Schema[_], + concreteTypes: List[PropMeta[L]], + definitions: List[(String, Schema[_])] + )(implicit M: ModelProtocolTerms[L, F], + F: FrameworkTerms[L, F], + P: PolyProtocolTerms[L, F], + Sc: ScalaTerms[L, F], + Sw: SwaggerTerms[L, F]): Free[F, (List[ProtocolParameter[L]], List[ClassDefinition[L]])] = { + import F._ + import M._ + import Sc._ + def processProperty(name: String, schema: Schema[_]): Free[F, Option[Either[String, ClassDefinition[L]]]] = { + val nestedClassName = clsName.append(name.toCamelCase.capitalize) + schema match { + case _: ObjectSchema => + fromModel(nestedClassName, schema, List.empty, concreteTypes, definitions).map(Some(_)) + case o: ComposedSchema => + for { + parents <- extractParents(o, definitions, concreteTypes) + maybeClassDefinition <- fromModel(nestedClassName, schema, parents, concreteTypes, definitions) + } yield Some(maybeClassDefinition) + case a: ArraySchema => + processProperty(name, a.getItems) + case _ => + Free.pure(None) + } + } for { props <- extractProperties(model) requiredFields = getRequiredFieldsRec(model) needCamelSnakeConversion = props.forall { case (k, _) => couldBeSnakeCase(k) } - params <- props.traverse({ - case (name, prop) => - val isRequired = requiredFields.contains(name) - SwaggerUtil.propMeta[L, F](prop).flatMap(transformProperty(clsName, needCamelSnakeConversion, concreteTypes)(name, prop, _, isRequired)) - }) - defn <- renderDTOClass(clsName, params, parents) - deps = params.flatMap(_.dep) - encoder <- encodeModel(clsName, needCamelSnakeConversion, params, parents) - decoder <- decodeModel(clsName, needCamelSnakeConversion, params, parents) - staticDefns <- renderDTOStaticDefns(clsName, List.empty, encoder, decoder) - tpe <- parseTypeName(clsName) - } yield - if (parents.isEmpty && props.isEmpty) Left("Entity isn't model") - else tpe.toRight("Empty entity name").map(ClassDefinition[L](clsName, _, defn, staticDefns, parents)) + paramsAndNestedClasses <- props.traverse[Free[F, ?], (ProtocolParameter[L], Option[ClassDefinition[L]])] { + case (name, schema) => + val typeName = clsName.append(name.toCamelCase.capitalize) + for { + tpe <- selectType(typeName) + maybeClassDefinition <- processProperty(name, schema) + resolvedType <- maybeClassDefinition.fold(SwaggerUtil.propMetaWithName(tpe, schema)) { + case Left(_) => objectType(None).map(Resolved(_, None, None, None, None)) + case Right(_) => SwaggerUtil.propMetaWithName(tpe, schema) + } + parameter <- transformProperty(clsName.last, needCamelSnakeConversion, concreteTypes)(name, schema, resolvedType, requiredFields.contains(name)) + } yield (parameter, maybeClassDefinition.flatMap(_.toOption)) + } + (params, nestedClassDefinitions) = paramsAndNestedClasses.unzip + } yield params -> nestedClassDefinitions.flatten } def modelTypeAlias[L <: LA, F[_]](clsName: String, abstractModel: Schema[_])( @@ -436,14 +501,14 @@ object ProtocolGenerator { case m: StringSchema => for { enum <- fromEnum(clsName, m) - model <- fromModel(clsName, m, List.empty, concreteTypes) + model <- fromModel(NonEmptyList.of(clsName), m, List.empty, concreteTypes, definitions) alias <- modelTypeAlias(clsName, m) } yield enum.orElse(model).getOrElse(alias) case comp: ComposedSchema => for { parents <- extractParents(comp, definitions, concreteTypes) - model <- fromModel(clsName, comp, parents, concreteTypes) + model <- fromModel(NonEmptyList.of(clsName), comp, parents, concreteTypes, definitions) alias <- modelTypeAlias(clsName, comp) } yield model.getOrElse(alias) @@ -453,7 +518,7 @@ object ProtocolGenerator { case m: ObjectSchema => for { enum <- fromEnum(clsName, m) - model <- fromModel(clsName, m, List.empty, concreteTypes) + model <- fromModel(NonEmptyList.of(clsName), m, List.empty, concreteTypes, definitions) alias <- modelTypeAlias(clsName, m) } yield enum.orElse(model).getOrElse(alias) diff --git a/modules/codegen/src/main/scala/com/twilio/guardrail/SwaggerUtil.scala b/modules/codegen/src/main/scala/com/twilio/guardrail/SwaggerUtil.scala index d9f4c85529..c52ed6770f 100644 --- a/modules/codegen/src/main/scala/com/twilio/guardrail/SwaggerUtil.scala +++ b/modules/codegen/src/main/scala/com/twilio/guardrail/SwaggerUtil.scala @@ -277,20 +277,56 @@ object SwaggerUtil { } yield result } - def propMeta[L <: LA, F[_]](property: Schema[_])(implicit Sc: ScalaTerms[L, F], Sw: SwaggerTerms[L, F], Fw: FrameworkTerms[L, F]): Free[F, ResolvedType[L]] = + def propMeta[L <: LA, F[_]](property: Schema[_])( + implicit Sc: ScalaTerms[L, F], + Sw: SwaggerTerms[L, F], + Fw: FrameworkTerms[L, F] + ): Free[F, ResolvedType[L]] = { + import Fw._ + import Sw._ + propMetaImpl(property) { + case o: ObjectSchema => + for { + _ <- log.debug( + s"Not attempting to process properties from ${o.showNotNull}" + ) + tpe <- objectType(None) // TODO: o.getProperties + } yield Resolved[L](tpe, None, None, None, None) + } + } + + def propMetaWithName[L <: LA, F[_]](tpe: L#Type, property: Schema[_])( + implicit Sc: ScalaTerms[L, F], + Sw: SwaggerTerms[L, F], + Fw: FrameworkTerms[L, F] + ): Free[F, ResolvedType[L]] = { + import Sc._ + val action: Free[F, ResolvedType[L]] = Free.pure(Resolved[L](tpe, None, None, None, None)) + propMetaImpl(property) { + case _: ObjectSchema => + action + case _: ComposedSchema => + action + } + } + + private def propMetaImpl[L <: LA, F[_]](property: Schema[_])( + strategy: PartialFunction[Schema[_], Free[F, ResolvedType[L]]] + )(implicit Sc: ScalaTerms[L, F], Sw: SwaggerTerms[L, F], Fw: FrameworkTerms[L, F]): Free[F, ResolvedType[L]] = Sw.log.function("propMeta") { import Fw._ import Sc._ import Sw._ - log.debug(s"property:\n${log.schemaToString(property)}") >> (property match { + val baseStrategy: PartialFunction[Schema[_], Free[F, ResolvedType[L]]] = { case p: ArraySchema => for { items <- getItems(p) - rec <- propMeta[L, F](items) + rec <- propMetaImpl[L, F](items)(strategy) res <- rec match { case Resolved(inner, dep, default, _, _) => - (liftVectorType(inner), default.traverse(liftVectorTerm)).mapN(Resolved[L](_, dep, _, None, None): ResolvedType[L]) + (liftVectorType(inner), default.traverse(liftVectorTerm)) + .mapN(Resolved[L](_, dep, _, None, None): ResolvedType[L]) case x: DeferredMap[L] => embedArray(x) case x: DeferredArray[L] => embedArray(x) case x: Deferred[L] => embedArray(x) @@ -299,22 +335,21 @@ object SwaggerUtil { case m: MapSchema => for { rec <- Option(m.getAdditionalProperties) - .fold[Free[F, ResolvedType[L]]](objectType(None).map(Resolved[L](_, None, None, None, None)))({ - case b: java.lang.Boolean => objectType(None).map(Resolved[L](_, None, None, None, None)) - case s: Schema[_] => propMeta[L, F](s) + .fold[Free[F, ResolvedType[L]]]( + objectType(None).map(Resolved[L](_, None, None, None, None)) + )({ + case b: java.lang.Boolean => + objectType(None).map(Resolved[L](_, None, None, None, None)) + case s: Schema[_] => propMetaImpl[L, F](s)(strategy) }) res <- rec match { - case Resolved(inner, dep, _, _, _) => liftMapType(inner).map(Resolved[L](_, dep, None, None, None)) - case x: DeferredMap[L] => embedMap(x) - case x: DeferredArray[L] => embedMap(x) - case x: Deferred[L] => embedMap(x) + case Resolved(inner, dep, _, _, _) => + liftMapType(inner).map(Resolved[L](_, dep, None, None, None)) + case x: DeferredMap[L] => embedMap(x) + case x: DeferredArray[L] => embedMap(x) + case x: Deferred[L] => embedMap(x) } } yield res - case o: ObjectSchema => - for { - _ <- log.debug(s"Not attempting to process properties from ${o.showNotNull}") - tpe <- objectType(None) // TODO: o.getProperties - } yield Resolved[L](tpe, None, None, None, None) case ref: Schema[_] if Option(ref.get$ref).isDefined => getSimpleRef(ref).map(Deferred[L]) @@ -324,8 +359,10 @@ object SwaggerUtil { val rawFormat = Option.empty[String] for { customTpeName <- customTypeName(b) - res <- (typeName[L, F](rawType, None, customTpeName), Default(b).extract[Boolean].traverse(litBoolean(_))) - .mapN(Resolved[L](_, None, _, Some(rawType), rawFormat)) + res <- ( + typeName[L, F](rawType, None, customTpeName), + Default(b).extract[Boolean].traverse(litBoolean(_)) + ).mapN(Resolved[L](_, None, _, Some(rawType), rawFormat)) } yield res case s: StringSchema => @@ -333,8 +370,10 @@ object SwaggerUtil { val rawFormat = Option(s.getFormat()) for { customTpeName <- customTypeName(s) - res <- (typeName[L, F](rawType, rawFormat, customTpeName), Default(s).extract[String].traverse(litString(_))) - .mapN(Resolved[L](_, None, _, Option(rawType), rawFormat)) + res <- ( + typeName[L, F](rawType, rawFormat, customTpeName), + Default(s).extract[String].traverse(litString(_)) + ).mapN(Resolved[L](_, None, _, Option(rawType), rawFormat)) } yield res case d: DateSchema => @@ -392,12 +431,17 @@ object SwaggerUtil { customTpeName <- customTypeName(u) tpe <- typeName[L, F]("string", rawFormat, customTpeName) } yield Resolved[L](tpe, None, None, Option(rawType), rawFormat) + } + val strategies = baseStrategy.orElse(strategy).orElse[Schema[_], Free[F, ResolvedType[L]]] { case x => for { tpe <- fallbackPropertyTypeHandler(x) } yield Resolved[L](tpe, None, None, None, None) // This may need to be rawType=string? - }) + } + log.debug(s"property:\n${log.schemaToString(property)}") >> strategies( + property + ) } def extractSecuritySchemes[L <: LA, F[_]](swagger: OpenAPI, prefixes: List[String])(implicit Sw: SwaggerTerms[L, F], diff --git a/modules/codegen/src/main/scala/com/twilio/guardrail/generators/JavaGenerator.scala b/modules/codegen/src/main/scala/com/twilio/guardrail/generators/JavaGenerator.scala index 54b3e4040b..e81fc179b8 100644 --- a/modules/codegen/src/main/scala/com/twilio/guardrail/generators/JavaGenerator.scala +++ b/modules/codegen/src/main/scala/com/twilio/guardrail/generators/JavaGenerator.scala @@ -182,6 +182,9 @@ object JavaGenerator { case ExtractTermName(term) => Target.pure(term.asString) + case SelectType(typeNames) => + safeParseType(typeNames.toList.mkString(".")) + case AlterMethodParameterName(param, name) => safeParseSimpleName(name.asString.escapeIdentifier).map( new Parameter( @@ -209,8 +212,10 @@ object JavaGenerator { case ArrayType(format) => safeParseClassOrInterfaceType("java.util.List").map(_.setTypeArguments(new NodeList[Type](STRING_TYPE))) case FallbackType(tpe, format) => safeParseType(tpe) - case WidenTypeName(tpe) => safeParseType(tpe.asString) - case WidenTermSelect(value) => Target.pure(value) + case WidenTypeName(tpe) => safeParseType(tpe.asString) + case WidenTermSelect(value) => Target.pure(value) + case WidenClassDefinition(value) => Target.pure(value) + case WidenObjectDefinition(value) => Target.pure(value) case RenderImplicits(pkgPath, pkgName, frameworkImports, jsonImports, customImports) => Target.pure(None) @@ -322,6 +327,9 @@ object JavaGenerator { handlerTree <- writeServerTree(pkgPath, pkg, pkgDecl, allImports, handlerDefinition) serverTrees <- serverDefinitions.traverse(writeServerTree(pkgPath, pkg, pkgDecl, allImports, _)) } yield handlerTree +: serverTrees + + case WrapToObject(_, _, _) => + Target.raiseError("Currently not supported for Java") } } } diff --git a/modules/codegen/src/main/scala/com/twilio/guardrail/generators/ScalaGenerator.scala b/modules/codegen/src/main/scala/com/twilio/guardrail/generators/ScalaGenerator.scala index d77cd3c240..9692eb52c0 100644 --- a/modules/codegen/src/main/scala/com/twilio/guardrail/generators/ScalaGenerator.scala +++ b/modules/codegen/src/main/scala/com/twilio/guardrail/generators/ScalaGenerator.scala @@ -8,6 +8,9 @@ import com.twilio.guardrail.generators.syntax.Scala._ import com.twilio.guardrail.languages.ScalaLanguage import com.twilio.guardrail.terms._ import java.nio.charset.StandardCharsets + +import cats.data.NonEmptyList + import scala.meta._ object ScalaGenerator { @@ -104,6 +107,19 @@ object ScalaGenerator { val Term.Name(name) = term Target.pure(name) + case SelectType(typeNames) => + val tpe = Type.Name(typeNames.last) + val names = typeNames.init.map(Term.Name.apply _) + + val result = names match { + case Nil => tpe + case x :: xs => + val term = xs.foldLeft[Term.Ref](x)(Term.Select.apply _) + Type.Select(term, tpe) + } + + Target.pure(result) + case AlterMethodParameterName(param, name) => Target.pure(param.copy(name = name)) @@ -121,8 +137,10 @@ object ScalaGenerator { case ArrayType(format) => Target.pure(t"Iterable[String]") case FallbackType(tpe, format) => Target.pure(Type.Name(tpe)) - case WidenTypeName(tpe) => Target.pure(tpe) - case WidenTermSelect(value) => Target.pure(value) + case WidenTypeName(tpe) => Target.pure(tpe) + case WidenTermSelect(value) => Target.pure(value) + case WidenClassDefinition(value) => Target.pure(value) + case WidenObjectDefinition(value) => Target.pure(value) case RenderImplicits(pkgPath, pkgName, frameworkImports, jsonImports, customImports) => val pkg: Term.Ref = @@ -381,6 +399,13 @@ object ScalaGenerator { ) ) ) + case WrapToObject(name, imports, definitions) => + Target.pure(q""" + object $name { + ..$imports + ..$definitions + } + """) } } } diff --git a/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerm.scala b/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerm.scala index 9e336c82ea..d90cfbf66c 100644 --- a/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerm.scala +++ b/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerm.scala @@ -5,6 +5,8 @@ import com.twilio.guardrail.SwaggerUtil.LazyResolvedType import com.twilio.guardrail.languages.LA import java.nio.file.Path +import cats.data.NonEmptyList + sealed trait ScalaTerm[L <: LA, T] case class VendorPrefixes[L <: LA]() extends ScalaTerm[L, List[String]] @@ -39,6 +41,7 @@ case class TypeNamesEqual[L <: LA](a: L#TypeName, b: L#TypeName) case class TypesEqual[L <: LA](a: L#Type, b: L#Type) extends ScalaTerm[L, Boolean] case class ExtractTypeName[L <: LA](tpe: L#Type) extends ScalaTerm[L, Option[L#TypeName]] case class ExtractTermName[L <: LA](term: L#TermName) extends ScalaTerm[L, String] +case class SelectType[L <: LA](typeNames: NonEmptyList[String]) extends ScalaTerm[L, L#Type] case class AlterMethodParameterName[L <: LA](param: L#MethodParameter, name: L#TermName) extends ScalaTerm[L, L#MethodParameter] case class UUIDType[L <: LA]() extends ScalaTerm[L, L#Type] @@ -55,8 +58,10 @@ case class BooleanType[L <: LA](format: Option[String]) extends Sc case class ArrayType[L <: LA](format: Option[String]) extends ScalaTerm[L, L#Type] case class FallbackType[L <: LA](tpe: String, format: Option[String]) extends ScalaTerm[L, L#Type] -case class WidenTypeName[L <: LA](tpe: L#TypeName) extends ScalaTerm[L, L#Type] -case class WidenTermSelect[L <: LA](value: L#TermSelect) extends ScalaTerm[L, L#Term] +case class WidenTypeName[L <: LA](tpe: L#TypeName) extends ScalaTerm[L, L#Type] +case class WidenTermSelect[L <: LA](value: L#TermSelect) extends ScalaTerm[L, L#Term] +case class WidenClassDefinition[L <: LA](value: L#ClassDefinition) extends ScalaTerm[L, L#Definition] +case class WidenObjectDefinition[L <: LA](value: L#ObjectDefinition) extends ScalaTerm[L, L#Definition] case class RenderImplicits[L <: LA](pkgPath: Path, pkgName: List[String], @@ -106,3 +111,5 @@ case class WriteServer[L <: LA](pkgPath: Path, dtoComponents: List[String], server: Server[L]) extends ScalaTerm[L, List[WriteTree]] + +case class WrapToObject[L <: LA](name: L#TermName, imports: List[L#Import], definitions: List[L#Definition]) extends ScalaTerm[L, L#ObjectDefinition] diff --git a/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerms.scala b/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerms.scala index 9aaad0607f..553d20d78e 100644 --- a/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerms.scala +++ b/modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerms.scala @@ -7,6 +7,8 @@ import com.twilio.guardrail.SwaggerUtil.LazyResolvedType import com.twilio.guardrail.languages.LA import java.nio.file.Path +import cats.data.NonEmptyList + class ScalaTerms[L <: LA, F[_]](implicit I: InjectK[ScalaTerm[L, ?], F]) { def vendorPrefixes(): Free[F, List[String]] = Free.inject[ScalaTerm[L, ?], F](VendorPrefixes[L]()) @@ -41,6 +43,7 @@ class ScalaTerms[L <: LA, F[_]](implicit I: InjectK[ScalaTerm[L, ?], F]) { def typesEqual(a: L#Type, b: L#Type): Free[F, Boolean] = Free.inject[ScalaTerm[L, ?], F](TypesEqual(a, b)) def extractTypeName(tpe: L#Type): Free[F, Option[L#TypeName]] = Free.inject[ScalaTerm[L, ?], F](ExtractTypeName(tpe)) def extractTermName(term: L#TermName): Free[F, String] = Free.inject[ScalaTerm[L, ?], F](ExtractTermName(term)) + def selectType(typeNames: NonEmptyList[String]): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](SelectType(typeNames)) def alterMethodParameterName(param: L#MethodParameter, name: L#TermName): Free[F, L#MethodParameter] = Free.inject[ScalaTerm[L, ?], F](AlterMethodParameterName(param, name)) @@ -58,8 +61,10 @@ class ScalaTerms[L <: LA, F[_]](implicit I: InjectK[ScalaTerm[L, ?], F]) { def arrayType(format: Option[String]): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](ArrayType(format)) def fallbackType(tpe: String, format: Option[String]): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](FallbackType(tpe, format)) - def widenTypeName(tpe: L#TypeName): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](WidenTypeName(tpe)) - def widenTermSelect(value: L#TermSelect): Free[F, L#Term] = Free.inject[ScalaTerm[L, ?], F](WidenTermSelect(value)) + def widenTypeName(tpe: L#TypeName): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](WidenTypeName(tpe)) + def widenTermSelect(value: L#TermSelect): Free[F, L#Term] = Free.inject[ScalaTerm[L, ?], F](WidenTermSelect(value)) + def widenClassDefinition(value: L#ClassDefinition): Free[F, L#Definition] = Free.inject[ScalaTerm[L, ?], F](WidenClassDefinition(value)) + def widenObjectDefinition(value: L#ObjectDefinition): Free[F, L#Definition] = Free.inject[ScalaTerm[L, ?], F](WidenObjectDefinition(value)) def renderImplicits(pkgPath: Path, pkgName: List[String], @@ -112,6 +117,9 @@ class ScalaTerms[L <: LA, F[_]](implicit I: InjectK[ScalaTerm[L, ?], F]) { dtoComponents: List[String], server: Server[L]): Free[F, List[WriteTree]] = Free.inject[ScalaTerm[L, ?], F](WriteServer(pkgPath, pkgName, customImports, frameworkImplicitName, dtoComponents, server)) + + def wrapToObject(name: L#TermName, imports: List[L#Import], definitions: List[L#Definition]): Free[F, L#ObjectDefinition] = + Free.inject[ScalaTerm[L, ?], F](WrapToObject(name, imports, definitions)) } object ScalaTerms { implicit def scalaTerm[L <: LA, F[_]](implicit I: InjectK[ScalaTerm[L, ?], F]): ScalaTerms[L, F] = new ScalaTerms[L, F] diff --git a/modules/codegen/src/test/scala/tests/core/TypesTest.scala b/modules/codegen/src/test/scala/tests/core/TypesTest.scala index bd8050c531..0cfbabdf31 100644 --- a/modules/codegen/src/test/scala/tests/core/TypesTest.scala +++ b/modules/codegen/src/test/scala/tests/core/TypesTest.scala @@ -9,63 +9,74 @@ import support.SwaggerSpecRunner class TypesTest extends FunSuite with Matchers with SwaggerSpecRunner { - val swagger: String = s""" - |swagger: "2.0" - |info: - | title: Whatever - | version: 1.0.0 - |host: localhost:1234 - |definitions: - | Types: - | type: object - | properties: - | array: - | type: array - | items: - | type: boolean - | map: - | type: objet - | additionalProperties: - | type: boolean - | obj: - | type: object - | bool: - | type: boolean - | string: - | type: string - | date: - | type: string - | format: date - | date_time: - | type: string - | format: date-time - | long: - | type: integer - | format: int64 - | int: - | type: integer - | format: int32 - | float: - | type: number - | format: float - | double: - | type: number - | format: double - | number: - | type: number - | integer: - | type: integer - | untyped: - | description: Untyped - | custom: - | type: string - | x-scala-type: Foo - | customComplex: - | type: string - | x-scala-type: Foo[Bar] - |""".stripMargin - test("Generate no definitions") { + val swagger: String = s""" + |swagger: "2.0" + |info: + | title: Whatever + | version: 1.0.0 + |host: localhost:1234 + |definitions: + | Types: + | type: object + | properties: + | array: + | type: array + | items: + | type: boolean + | map: + | type: objet + | additionalProperties: + | type: boolean + | obj: + | type: object + | bool: + | type: boolean + | string: + | type: string + | date: + | type: string + | format: date + | date_time: + | type: string + | format: date-time + | long: + | type: integer + | format: int64 + | int: + | type: integer + | format: int32 + | float: + | type: number + | format: float + | double: + | type: number + | format: double + | number: + | type: number + | integer: + | type: integer + | untyped: + | description: Untyped + | custom: + | type: string + | x-scala-type: Foo + | customComplex: + | type: string + | x-scala-type: Foo[Bar] + | nested: + | type: object + | properties: + | prop1: + | type: string + | nestedArray: + | type: array + | items: + | type: object + | properties: + | prop1: + | type: string + |""".stripMargin val ( ProtocolDefinitions(ClassDefinition(_, _, cls, staticDefns, _) :: Nil, _, _, _), _, @@ -89,7 +100,9 @@ class TypesTest extends FunSuite with Matchers with SwaggerSpecRunner { integer: Option[BigInt] = None, untyped: Option[io.circe.Json] = None, custom: Option[Foo] = None, - customComplex: Option[Foo[Bar]] = None + customComplex: Option[Foo[Bar]] = None, + nested: Option[Types.Nested] = None, + nestedArray: Option[IndexedSeq[Types.NestedArray]] = Option(IndexedSeq.empty), ) """ @@ -97,13 +110,148 @@ class TypesTest extends FunSuite with Matchers with SwaggerSpecRunner { object Types { implicit val encodeTypes: ObjectEncoder[Types] = { val readOnlyKeys = Set[String]() - Encoder.forProduct15("array", "obj", "bool", "string", "date", "date_time", "long", "int", "float", "double", "number", "integer", "untyped", "custom", "customComplex")((o: Types) => (o.array, o.obj, o.bool, o.string, o.date, o.date_time, o.long, o.int, o.float, o.double, o.number, o.integer, o.untyped, o.custom, o.customComplex)).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + Encoder.forProduct17("array", "obj", "bool", "string", "date", "date_time", "long", "int", "float", "double", "number", "integer", "untyped", "custom", "customComplex", "nested", "nestedArray")((o: Types) => (o.array, o.obj, o.bool, o.string, o.date, o.date_time, o.long, o.int, o.float, o.double, o.number, o.integer, o.untyped, o.custom, o.customComplex, o.nested, o.nestedArray)).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeTypes: Decoder[Types] = Decoder.forProduct17("array", "obj", "bool", "string", "date", "date_time", "long", "int", "float", "double", "number", "integer", "untyped", "custom", "customComplex", "nested", "nestedArray")(Types.apply _) + + case class Nested(prop1: Option[String] = None) + object Nested { + implicit val encodeNested: ObjectEncoder[Nested] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct1("prop1")((o: Nested) => o.prop1).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeNested: Decoder[Nested] = Decoder.forProduct1("prop1")(Nested.apply _) + } + + case class NestedArray(prop1: Option[String] = None) + object NestedArray { + implicit val encodeNestedArray: ObjectEncoder[NestedArray] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct1("prop1")((o: NestedArray) => o.prop1).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeNestedArray: Decoder[NestedArray] = Decoder.forProduct1("prop1")(NestedArray.apply _) + } + } + """ + + cls.structure shouldEqual definition.structure + cmp.structure shouldEqual companion.structure + } + + test("Generates from composed schema") { + val swagger: String = s""" + |swagger: "2.0" + |info: + | title: Whatever + | version: 1.0.0 + |host: localhost:1234 + |definitions: + | That: + | type: object + | properties: + | string: + | type: string + | Types: + | type: object + | properties: + | composed: + | allOf: + | - $$ref: '#/definitions/That' + | - type: object + | properties: + | int: + | type: integer + | format: int32 + | + |""".stripMargin + val ( + ProtocolDefinitions(ClassDefinition(_, _, _, _, _) :: ClassDefinition(_, _, cls, staticDefns, _) :: Nil, _, _, _), + _, + _ + ) = runSwaggerSpec(swagger)(Context.empty, AkkaHttp) + val cmp = companionForStaticDefns(staticDefns) + + val definition = q"""case class Types(composed: Option[Types.Composed] = None)""" + + val companion = q""" + object Types { + implicit val encodeTypes: ObjectEncoder[Types] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct1("composed")((o: Types) => o.composed).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeTypes: Decoder[Types] = Decoder.forProduct1("composed")(Types.apply _) + case class Composed(string: Option[String] = None, int: Option[Int] = None) + object Composed { + implicit val encodeComposed: ObjectEncoder[Composed] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct2("string", "int")((o: Composed) => (o.string, o.int)).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeComposed: Decoder[Composed] = Decoder.forProduct2("string", "int")(Composed.apply _) } - implicit val decodeTypes: Decoder[Types] = Decoder.forProduct15("array", "obj", "bool", "string", "date", "date_time", "long", "int", "float", "double", "number", "integer", "untyped", "custom", "customComplex")(Types.apply _) } """ cls.structure shouldEqual definition.structure cmp.structure shouldEqual companion.structure } + + test("Deeply nested structure is generated") { + val swagger = + s""" + |swagger: "2.0" + |info: + | title: Whatever + | version: 1.0.0 + |host: localhost:1234 + |definitions: + | First: + | type: object + | properties: + | Second: + | type: object + | properties: + | Third: + | type: object + | properties: + | Fourth: + | type: string + |""".stripMargin + val ( + ProtocolDefinitions(ClassDefinition(_, _, cls, staticDefns, _) :: Nil, _, _, _), + _, + _ + ) = runSwaggerSpec(swagger)(Context.empty, AkkaHttp) + val cmp = companionForStaticDefns(staticDefns) + + val definition = q"""case class First(Second: Option[First.Second] = None)""" + + val companion = q""" + object First { + implicit val encodeFirst: ObjectEncoder[First] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct1("Second")((o: First) => o.Second).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeFirst: Decoder[First] = Decoder.forProduct1("Second")(First.apply _) + case class Second(Third: Option[First.Second.Third] = None) + object Second { + implicit val encodeSecond: ObjectEncoder[Second] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct1("Third")((o: Second) => o.Third).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeSecond: Decoder[Second] = Decoder.forProduct1("Third")(Second.apply _) + case class Third(Fourth: Option[String] = None) + object Third { + implicit val encodeThird: ObjectEncoder[Third] = { + val readOnlyKeys = Set[String]() + Encoder.forProduct1("Fourth")((o: Third) => o.Fourth).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key))) + } + implicit val decodeThird: Decoder[Third] = Decoder.forProduct1("Fourth")(Third.apply _) + } + } + } + """ + + cls.structure shouldEqual definition.structure + cmp.structure shouldEqual companion.structure + } }