diff --git a/.gitignore b/.gitignore index b6f1427..a659307 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ out/ .scala-build/ - +.deder/ diff --git a/README.md b/README.md index 0a0b438..166e42d 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,29 @@ You can use `openapi4s-cli` with Coursier launcher to generate your sources: ```shell cs launch ba.sake::openapi4s-cli:0.6.1 -M ba.sake.openapi4s.cli.OpenApi4sMain -- \ - --generator sharaf \ + --models tupson \ + --framework sharaf \ --url openapi.json \ --baseFolder src \ --basePackage com.example ``` +You can now combine model and framework generation independently: +```shell +# circe models + http4s routes +--models circe --framework http4s + +# circe models + sharaf controllers +--models circe --framework sharaf + +# models only +--models tupson --framework none + +# framework only (expects existing com.example.models) +--models none --framework http4s +``` + +`--generator` is still supported as a deprecated compatibility alias: +- `--generator sharaf` => `--models tupson --framework sharaf` +- `--generator http4s` => `--models circe --framework http4s` diff --git a/cli/src/main/scala/ba/sake/openapi4s/cli/OpenApi4sMain.scala b/cli/src/main/scala/ba/sake/openapi4s/cli/OpenApi4sMain.scala index b21fd83..8832c51 100644 --- a/cli/src/main/scala/ba/sake/openapi4s/cli/OpenApi4sMain.scala +++ b/cli/src/main/scala/ba/sake/openapi4s/cli/OpenApi4sMain.scala @@ -1,6 +1,6 @@ package ba.sake.openapi4s.cli -import ba.sake.openapi4s.OpenApiGenerator +import ba.sake.openapi4s.OpenApiWriter import java.nio.file.Paths import mainargs.{main, arg, ParserForMethods} @@ -9,8 +9,10 @@ object OpenApi4sMain { @main def run( - @arg(doc = "Generator name: 'sharaf' or 'http4s'. Default is 'sharaf'") - generator: String = "sharaf", + @arg(doc = "Model backend: 'circe', 'tupson' or 'none'. If unset, defaults to 'tupson'.") + models: String = "tupson", + @arg(doc = "Framework backend: 'http4s', 'sharaf' or 'none'. If unset, defaults to 'sharaf'.") + framework: String = "sharaf", @arg(doc = "OpenAPI URL or file path. Default is 'openapi.json'") url: String = "openapi.json", @arg(doc = "Base folder for generated sources. Default is 'src/main/scala'") @@ -18,15 +20,16 @@ object OpenApi4sMain { @arg(doc = "Base package for generated sources") basePackage: String ) = { - val openApiGenerator = OpenApiGenerator( - name = generator, - config = OpenApiGenerator.Config( + val writer = OpenApiWriter( + config = OpenApiWriter.Config( url = url, baseFolder = Paths.get(baseFolder), - basePackage = basePackage + basePackage = basePackage, + models = models, + framework = framework ) ) - openApiGenerator.generate() + writer.write() } def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args) } diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/FrameworkBackend.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/FrameworkBackend.scala new file mode 100644 index 0000000..40628bf --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/FrameworkBackend.scala @@ -0,0 +1,89 @@ +package ba.sake.openapi4s + +import ba.sake.openapi4s.OpenApiWriter.Config +import ba.sake.openapi4s.http4s.Http4sGenerator +import ba.sake.openapi4s.sharaf.SharafGenerator +import ba.sake.regenesca.GeneratedFileSource + +sealed trait FrameworkBackendId +object FrameworkBackendId { + case object NoFramework extends FrameworkBackendId + case object Http4s extends FrameworkBackendId + case object Sharaf extends FrameworkBackendId + + val all: List[FrameworkBackendId] = List(NoFramework, Http4s, Sharaf) + + def fromString(s: String): FrameworkBackendId = s.toLowerCase match { + case "http4s" => Http4s + case "sharaf" => Sharaf + case "none" => NoFramework + case _ => + throw new RuntimeException( + s"Unknown framework backend '${s}'. Available framework backends: 'http4s', 'sharaf', 'none'" + ) + } +} + +trait FrameworkBackend { + def id: FrameworkBackendId + def supportedModelIds: Set[ModelBackendId] + def generator( + config: OpenApiWriter.Config, + openapiDefinition: OpenApiDefinition, + modelContract: ModelContract + ): OpenApiGenerator +} + +object FrameworkBackend { + + val none: FrameworkBackend = new FrameworkBackend { + override val id: FrameworkBackendId = FrameworkBackendId.NoFramework + override val supportedModelIds: Set[ModelBackendId] = + Set(ModelBackendId.NoModel, ModelBackendId.Circe, ModelBackendId.Tupson) + override def generator( + config: OpenApiWriter.Config, + openapiDefinition: OpenApiDefinition, + modelContract: ModelContract + ): OpenApiGenerator = new OpenApiGenerator { + override def generate(): Seq[GeneratedFileSource] = Seq.empty + } + } + + val http4s: FrameworkBackend = new FrameworkBackend { + override val id: FrameworkBackendId = FrameworkBackendId.Http4s + override val supportedModelIds: Set[ModelBackendId] = Set(ModelBackendId.Circe) + override def generator( + config: OpenApiWriter.Config, + openapiDefinition: OpenApiDefinition, + modelContract: ModelContract + ): OpenApiGenerator = { + new Http4sGenerator( + config = config, + openApiDefinition = openapiDefinition, + frameworkModelImports = modelContract.imports.frameworkImports(id) + ) + } + } + + val sharaf: FrameworkBackend = new FrameworkBackend { + override val id: FrameworkBackendId = FrameworkBackendId.Sharaf + override val supportedModelIds: Set[ModelBackendId] = Set(ModelBackendId.Tupson) + override def generator( + config: OpenApiWriter.Config, + openapiDefinition: OpenApiDefinition, + modelContract: ModelContract + ): OpenApiGenerator = { + new SharafGenerator( + config = config, + openApiDefinition = openapiDefinition, + frameworkModelImports = modelContract.imports.frameworkImports(id) + ) + } + } + + val byId: Map[FrameworkBackendId, FrameworkBackend] = Map( + FrameworkBackendId.NoFramework -> none, + FrameworkBackendId.Http4s -> http4s, + FrameworkBackendId.Sharaf -> sharaf + ) +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/GenerationImports.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/GenerationImports.scala new file mode 100644 index 0000000..7036447 --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/GenerationImports.scala @@ -0,0 +1,11 @@ +package ba.sake.openapi4s + +import scala.meta._ +import scala.meta.dialects.Scala34 + +object GenerationImports { + def modelWildcardImport(basePackage: String): Import = { + val importer = s"${basePackage}.models.*".parse[Importer].get + q"import ..${List(importer)}" + } +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/ModelBackend.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/ModelBackend.scala new file mode 100644 index 0000000..f991c74 --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/ModelBackend.scala @@ -0,0 +1,80 @@ +package ba.sake.openapi4s + +import scala.meta._ +import scala.meta.dialects.Scala34 +import ba.sake.openapi4s.OpenApiWriter.Config +import ba.sake.openapi4s.circe.CirceModelGenerator +import ba.sake.openapi4s.tupson.TupsonModelGenerator +import ba.sake.regenesca.GeneratedFileSource + +sealed trait ModelBackendId +object ModelBackendId { + case object Circe extends ModelBackendId + case object Tupson extends ModelBackendId + case object NoModel extends ModelBackendId + + val all: List[ModelBackendId] = List(Circe, Tupson, NoModel) + + def fromString(s: String): ModelBackendId = s.toLowerCase match { + case "circe" => Circe + case "tupson" => Tupson + case "none" => NoModel + case _ => + throw new RuntimeException( + s"Unknown model backend '${s}'. Available model backends: 'circe', 'tupson', 'none'" + ) + } +} + +case class ModelContract( + packageName: String, + id: ModelBackendId, + imports: ModelImportContract +) + +trait ModelBackend { + def id: ModelBackendId + def imports: ModelImportContract + def generator(config: Config, openapiDefinition: OpenApiDefinition): OpenApiGenerator + def contract(config: Config): ModelContract = ModelContract(s"${config.basePackage}.models", id, imports) +} + +object ModelBackend { + + private def generatePkgSelect(pkg: String): Term.Ref = { + pkg + .split("\\.") + .map(Term.Name(_)) + .reduceLeft[Term.Ref](Term.Select(_, _)) + } + + val none: ModelBackend = new ModelBackend { + override val id: ModelBackendId = ModelBackendId.NoModel + override val imports: ModelImportContract = ModelImportContracts.none + override def generator(config: Config, openapiDefinition: OpenApiDefinition): OpenApiGenerator = + new OpenApiGenerator { + override def generate(): Seq[GeneratedFileSource] = Seq.empty + } + } + + val circe: ModelBackend = new ModelBackend { + override val id: ModelBackendId = ModelBackendId.Circe + override val imports: ModelImportContract = ModelImportContracts.circe + override def generator(config: Config, openapiDefinition: OpenApiDefinition): OpenApiGenerator = + new CirceModelGenerator(config, openapiDefinition) + + } + + val tupson: ModelBackend = new ModelBackend { + override val id: ModelBackendId = ModelBackendId.Tupson + override val imports: ModelImportContract = ModelImportContracts.tupson + override def generator(config: Config, openapiDefinition: OpenApiDefinition): OpenApiGenerator = + new TupsonModelGenerator(config, openapiDefinition) + } + + val byId: Map[ModelBackendId, ModelBackend] = Map( + ModelBackendId.NoModel -> none, + ModelBackendId.Circe -> circe, + ModelBackendId.Tupson -> tupson + ) +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/ModelImportContracts.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/ModelImportContracts.scala new file mode 100644 index 0000000..c3d57fd --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/ModelImportContracts.scala @@ -0,0 +1,30 @@ +package ba.sake.openapi4s + +import scala.meta._ +import scala.meta.dialects.Scala34 + +case class ModelImportContract( + frameworkImportsById: Map[FrameworkBackendId, List[Import]] = Map.empty +) { + def frameworkImports(frameworkId: FrameworkBackendId): List[Import] = + frameworkImportsById.getOrElse(frameworkId, List.empty) +} + +object ModelImportContracts { + val none = ModelImportContract( + frameworkImportsById = Map.empty.withDefault(_ => List.empty) + ) + + val circe: ModelImportContract = ModelImportContract( + frameworkImportsById = Map( + FrameworkBackendId.Http4s -> List(q"import org.http4s.circe.CirceEntityCodec.*") + ) + ) + + val tupson: ModelImportContract = ModelImportContract( + frameworkImportsById = Map( + FrameworkBackendId.Sharaf -> List.empty + ) + ) + +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiGenerator.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiGenerator.scala index b8cafa2..c961bb6 100644 --- a/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiGenerator.scala +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiGenerator.scala @@ -1,28 +1,10 @@ package ba.sake.openapi4s -import java.nio.file.Path -import ba.sake.openapi4s.http4s.Http4sGenerator -import ba.sake.openapi4s.sharaf.SharafGenerator +import ba.sake.regenesca.GeneratedFileSource +/** Base trait for OpenApi generators. Each generator (model or framework) implements this trait to produce generated + * sources. + */ trait OpenApiGenerator { - def generate(): Unit -} - -object OpenApiGenerator { - - def apply(name: String, config: Config): OpenApiGenerator = { - val openapiDefinition = OpenApiDefinition.parse(config.url) - name.toLowerCase match { - case "http4s" => new Http4sGenerator(config, openapiDefinition) - case "sharaf" => new SharafGenerator(config, openapiDefinition) - case other => throw new RuntimeException(s"Unknown generator '${other}'. Available generators: 'http4s', 'sharaf'") - } - } - - case class Config( - url: String, - baseFolder: Path, - basePackage: String - ) - + def generate(): Seq[GeneratedFileSource] } diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiWriter.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiWriter.scala new file mode 100644 index 0000000..1fd2776 --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/OpenApiWriter.scala @@ -0,0 +1,71 @@ +package ba.sake.openapi4s + +import java.nio.file.Path +import ba.sake.regenesca.{GeneratedFileSource, RegenescaGenerator, SourceMerger} +import scala.meta.contrib.implicits.TreeExtensions +import scala.meta.dialects.Scala34 + +class OpenApiWriter( + config: OpenApiWriter.Config, + modelBackend: ModelBackend, + frameworkBackend: FrameworkBackend +) { + private val openapiDefinition = OpenApiDefinition.parse(config.url) + private val merger = SourceMerger(mergeDefBodies = true) + private val regenescaGenerator = RegenescaGenerator(merger) + + def write(): Seq[GeneratedFileSource] = { + println( + s"Started generating OpenApi for '${config.url}' with models='${config.models}', framework='${config.framework}' into '${config.baseFolder}' ..." + ) + val modelSources = modelBackend.generator(config, openapiDefinition).generate() + val modelContract = modelBackend.contract(config) + val frameworkSources = frameworkBackend.generator(config, openapiDefinition, modelContract).generate() + val packagePath = config.basePackage.replaceAll("\\.", "/") + val adaptedGenSourceFiles = (modelSources ++ frameworkSources).map { gsf => + gsf.copy(file = config.baseFolder.resolve(packagePath).resolve(gsf.file.toString)) + } + regenescaGenerator.generate(adaptedGenSourceFiles) + println( + s"Finished generating OpenApi for '${config.url}' with models='${config.models}', framework='${config.framework}'." + ) + adaptedGenSourceFiles + } +} + +object OpenApiWriter { + + private val modelBackends = ModelBackend.byId + private val frameworkBackends = FrameworkBackend.byId + + def apply(config: Config): OpenApiWriter = { + val modelId = ModelBackendId.fromString(config.models) + val frameworkId = FrameworkBackendId.fromString(config.framework) + + if (modelId == ModelBackendId.NoModel && frameworkId == FrameworkBackendId.NoFramework) { + throw new RuntimeException("Invalid config: models=none and framework=none means nothing to generate.") + } + + val modelBackend = modelBackends(modelId) + val frameworkBackend = frameworkBackends(frameworkId) + + if (!frameworkBackend.supportedModelIds.contains(modelBackend.id)) { + System.err.println( + s"WARNING: potentially incompatible backend combination: models='${config.models}', framework='${config.framework}'. " + + s"Framework '${frameworkBackend.id}' may not fully support model backend '${modelBackend.id}'. " + + s"Generated sources may require manual import/type adjustments; prefer compatible model/framework combinations when possible." + ) + } + + new OpenApiWriter(config, modelBackend, frameworkBackend) + } + + case class Config( + url: String, + baseFolder: Path, + basePackage: String, + models: String, + framework: String + ) + +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaDefinition.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaDefinition.scala index a3e0c66..5fcb424 100644 --- a/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaDefinition.scala +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaDefinition.scala @@ -4,9 +4,11 @@ case class NamedSchemaDefinitions( defs: Seq[SchemaDefinition.Named] ) -trait NameableSchemaDefinition extends SchemaDefinition // marker trait sealed abstract class SchemaDefinition + +trait NameableSchemaDefinition extends SchemaDefinition // marker trait + object SchemaDefinition { case class Str(default: Option[String], minLength: Option[Int], maxLength: Option[Int], pattern: Option[String]) extends SchemaDefinition diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaUtils.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaUtils.scala new file mode 100644 index 0000000..75fabf5 --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/SchemaUtils.scala @@ -0,0 +1,63 @@ +package ba.sake.openapi4s + +import scala.meta._ +import scala.meta.dialects.Scala34 +import org.apache.commons.text.CaseUtils +import ba.sake.openapi4s.exceptions.UnsupportedSchemaException + +object SchemaUtils { + + def resolveType( + schemaDef: SchemaDefinition, + propertyName: Option[String], + parentTypeName: Option[String], + // e.g. path enum cannot be null.. + allowNullable: Boolean, + context: String, + fallbackAnyType: Type + ): Type = schemaDef match { + case _: SchemaDefinition.Str => t"String" + case _: SchemaDefinition.Password => t"String" + case _: SchemaDefinition.Email => t"String" + case _: SchemaDefinition.Base64Bytes => t"String" // TODO use some kind of newtype.. ? + case _: SchemaDefinition.Int32 => t"Int" + case _: SchemaDefinition.Int64 => t"Long" + case _: SchemaDefinition.Num32 => t"Float" + case _: SchemaDefinition.Num64 => t"Double" + case _: SchemaDefinition.Bool => t"Boolean" + case _: SchemaDefinition.Uuid => t"UUID" + case _: SchemaDefinition.Date => t"LocalDate" + case _: SchemaDefinition.DateTime => t"Instant" + case SchemaDefinition.Opt(tpe) => + val coreTpe = + resolveType(tpe, propertyName, parentTypeName, allowNullable = allowNullable, context, fallbackAnyType) + if (allowNullable) t"Option[${coreTpe}]" + else coreTpe + case arr: SchemaDefinition.Arr => + val coreTpe = + resolveType(arr.schema, propertyName, parentTypeName, allowNullable = allowNullable, context, fallbackAnyType) + if (arr.uniqueItems) t"Set[${coreTpe}]" + else t"Seq[${coreTpe}]" + case SchemaDefinition.Enum(_, _) => + (parentTypeName.zip(propertyName)) match { + case Some((parentType, propName)) => + Type.Name(generateEnumName(parentType, propName)) + case _ => + throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for unnamed 'enum' [${context}]") + } + case SchemaDefinition.Ref(name) => Type.Name(name) + case SchemaDefinition.Named(name, _) => Type.Name(name) + case SchemaDefinition.Obj(_) => + throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for 'object' [${context}]") + case _: SchemaDefinition.OneOf => + throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for 'oneOf' [${context}]") + case _: SchemaDefinition.Unknown => fallbackAnyType + } + + def generateEnumName(parentType: String, propName: String): String = { + val camelizedParentType = CaseUtils.toCamelCase(parentType, true, '_') + val camelizedPropName = CaseUtils.toCamelCase(propName, true, '_') + s"${camelizedParentType}${camelizedPropName}" + } + +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/circe/CirceModelGenerator.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/circe/CirceModelGenerator.scala new file mode 100644 index 0000000..fa8c57d --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/circe/CirceModelGenerator.scala @@ -0,0 +1,171 @@ +package ba.sake.openapi4s +package circe + +import java.nio.file.Paths +import scala.meta._ +import scala.meta.dialects.Scala34 +import ba.sake.regenesca._ +import ba.sake.openapi4s.exceptions.UnsupportedSchemaException + +class CirceModelGenerator(config: OpenApiWriter.Config, openApiDefinition: OpenApiDefinition) + extends OpenApiGenerator { + + // keep track of done schemas (to avoid generating a subtype multiple times) + private var generatedNamedSchemas = Set.empty[String] + + override def generate(): Seq[GeneratedFileSource] = { + val modelsPkg = generatePkgSelect(s"${config.basePackage}.models") + val modelImports = List[Import]( + q"import java.time.*", + q"import java.util.UUID", + q"import io.circe.{Codec, Json}", + q"import io.circe.derivation.{Configuration, ConfiguredCodec, ConfiguredEnumCodec}" + ) + val modelFileSources = openApiDefinition.namedSchemaDefinitions.defs.flatMap { namedSchemaDef => + val namedSchemaName = namedSchemaDef.name.capitalize + val modelSources = generateModelSources(namedSchemaDef, None) + val allStmts = modelImports ++ modelSources + Option.when(modelSources.nonEmpty) { + GeneratedFileSource( + Paths.get(s"models/${namedSchemaName}.scala"), + source""" + // generated with OpenApi4s + package ${modelsPkg} { ..${allStmts} } + """ + ) + } + } + modelFileSources + } + + private def generateModelSources(namedSchemaDef: SchemaDefinition.Named, superType: Option[Type]): List[Stat] = { + val namedSchemaName = namedSchemaDef.name.capitalize + if (generatedNamedSchemas(namedSchemaName)) return List.empty + val typeName = Type.Name(namedSchemaName) + val termName = Term.Name(namedSchemaName) + val generatedModelSources = namedSchemaDef.schema match { + case obj: SchemaDefinition.Obj => + val params = obj.properties.flatMap { property => + try { + val propertyTpe = SchemaUtils.resolveType( + property.schema, + Some(property.name), + Some(namedSchemaName), + allowNullable = true, + context = s"${namedSchemaName}.${property.name}", + fallbackAnyType = t"Json" + ) + Some(param"${Term.Name(property.name)}: ${propertyTpe}") + } catch { + case e: UnsupportedSchemaException => + println(e.toString) + None + } + } + // enums defined in-place, we invent a new name for them.. + val adHocEnums = obj.properties.flatMap { property => + val enumValuesOpt = property.schema match { + case SchemaDefinition.Enum(values, _) => Some(values) + case SchemaDefinition.Opt(SchemaDefinition.Enum(values, _)) => Some(values) + case SchemaDefinition.Arr(SchemaDefinition.Enum(values, _), _, _, _) => Some(values) + case _ => None + } + enumValuesOpt + .map { values => + val adhocEnumName = SchemaUtils.generateEnumName(namedSchemaName, property.name) + val adhocEnumType = Type.Name(adhocEnumName) + val adhocEnumTerm = Term.Name(adhocEnumName) + val enumCaseDefs = Defn.RepeatedEnumCase( + List.empty, + values.map { enumDefCaseValue => + Term.Name(enumDefCaseValue) + } + ) + List( + q" enum ${adhocEnumType} { ${enumCaseDefs} } ", + q""" + object ${adhocEnumTerm} { + given Configuration = Configuration.default + given Codec[${adhocEnumType}] = ConfiguredEnumCodec.derived + } + """ + ) + } + .getOrElse(List.empty) + } + val classDefinition: List[Stat] = superType match { + case Some(st) => + val extendsInit = init"${st}()" + List(q""" + case class ${typeName}( ..${Term.ParamClause(params)} ) extends ${extendsInit} + """) + case None => + List( + q" case class ${typeName}( ..${Term.ParamClause(params)} ) ", + q""" + object ${termName} { + given Configuration = Configuration.default + given Codec[${typeName}] = ConfiguredCodec.derived + } + """ + ) + } + classDefinition ++ adHocEnums + + case enumDef: SchemaDefinition.Enum => + val enumCaseDefs = Defn.RepeatedEnumCase( + List.empty, + enumDef.values.map { enumDefCaseValue => + Term.Name(enumDefCaseValue) + } + ) + List( + q" enum ${typeName} { ${enumCaseDefs} } ", + q""" + object ${termName} { + given Configuration = Configuration.default + given Codec[${typeName}] = ConfiguredEnumCodec.derived + } + """ + ) + case _: SchemaDefinition.Arr => + // TODO type alias ??? + List.empty + case oneOfSchema: SchemaDefinition.OneOf => + val oneOfCases = oneOfSchema.schemas.flatMap { + case SchemaDefinition.Ref(refName) => + openApiDefinition.namedSchemaDefinitions.defs.find(_.name == refName) match { + case Some(referencedNamedSchema) => generateModelSources(referencedNamedSchema, Some(typeName)) + case None => + println(s"Non-existing sub-schema type: '${refName}' [${namedSchemaName}]") + None + } + case other => + println(s"Unsupported oneOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") + None + } + List( + q"sealed trait ${typeName} ", + q""" + object ${termName} { + given Configuration = Configuration.default.withDiscriminator(${Lit.String( + oneOfSchema.discriminatorPropertyName + )}) + given Codec[${typeName}] = ConfiguredCodec.derived + ..${oneOfCases} + } + """ + ) + } + generatedNamedSchemas += namedSchemaName + generatedModelSources + } + + private def generatePkgSelect(pkg: String) = { + val packageComponents = pkg.split("\\.").toList + val firstSelect = q"${Term.Name(packageComponents(0))}.${Term.Name(packageComponents(1))}" + packageComponents.tail.tail.foldLeft(firstSelect) { (a, b) => + q"${a}.${Term.Name(b)}" + } + } +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/http4s/Http4sGenerator.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/http4s/Http4sGenerator.scala index 0afe495..45f8792 100644 --- a/openapi4s/src/main/scala/ba/sake/openapi4s/http4s/Http4sGenerator.scala +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/http4s/Http4sGenerator.scala @@ -7,49 +7,15 @@ import scala.meta.dialects.Scala34 import org.apache.commons.text.CaseUtils import ba.sake.regenesca._ import ba.sake.openapi4s.exceptions.UnsupportedSchemaException +import ba.sake.openapi4s.circe.CirceModelGenerator -class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenApiDefinition) extends OpenApiGenerator { +class Http4sGenerator( + config: OpenApiWriter.Config, + openApiDefinition: OpenApiDefinition, + frameworkModelImports: List[Import] +) extends OpenApiGenerator { - private val merger = SourceMerger(mergeDefBodies = true) - private val regenescaGenerator = RegenescaGenerator(merger) - - // keep track of done schemas (to avoid generating a subtype multiple times) - private var generatedNamedSchemas = Set.empty[String] - - override def generate(): Unit = { - println(s"Started generating Http4s server for '${config.url}' OpenApi into '${config.baseFolder}' ...") - val packagePath = config.basePackage.replaceAll("\\.", "/") - val adaptedGenSourceFiles = generateSources.map { gsf => - gsf.copy(file = config.baseFolder.resolve(packagePath).resolve(gsf.file.toString)) - } - regenescaGenerator.generate(adaptedGenSourceFiles) - println(s"Finished generating Http4s server for '${config.url}' OpenApi.") - } - - private[http4s] def generateSources: Seq[GeneratedFileSource] = { - val modelsPkg = generatePkgSelect(s"${config.basePackage}.models") - val modelImports = List[Import]( - q"import java.time.*", - q"import java.util.UUID", - q"import io.circe.{Codec, Json}", - q"import io.circe.derivation.{Configuration, ConfiguredCodec, ConfiguredEnumCodec}" - ) - val modelFileSources = openApiDefinition.namedSchemaDefinitions.defs.flatMap { namedSchemaDef => - val namedSchemaName = namedSchemaDef.name.capitalize - val modelSources = generateModelSources(namedSchemaDef, None) - val allStmts = modelImports ++ modelSources - Option.when(modelSources.nonEmpty) { - GeneratedFileSource( - Paths.get(s"models/${namedSchemaName}.scala"), - source""" package ${modelsPkg} { ..${allStmts} } """ - ) - } - } - val routeFileSources = generateRoutesSources - modelFileSources ++ routeFileSources - } - - private def generateRoutesSources: List[GeneratedFileSource] = { + override def generate(): Seq[GeneratedFileSource] = { val groupedByTag = openApiDefinition.pathDefinitions.defs.groupBy(_.getTag) groupedByTag.flatMap { case (tag, pathDefinitions) => generateRouteSources(tag, pathDefinitions) @@ -67,7 +33,7 @@ class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp case PathSegment.Literal(value) => Lit.String(value) case PathSegment.Param(name, schema) => val tpe = - resolveType( + SchemaUtils.resolveType( schema, None, None, @@ -135,7 +101,7 @@ class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp // validation // TODO figure out how to validate Option-al nicely val validatedQPs = pathDef.queryParams.filter(_.required).map(qp => (qp.name, qp.schema)) - val validatorStmts = generateValidatorStmts(t"QP", validatedQPs) + val validatorStmts = SchemaUtils.generateValidsonStms(t"QP", validatedQPs) adhocEnums.flatten ++ List(q"case class QP(..${qpParams}) derives QueryStringRW") ++ Option.when(validatorStmts.nonEmpty)(q""" object QP { ..${validatorStmts} } """).toList ++ @@ -147,7 +113,7 @@ class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp val reqBodyStmts = pathDef.reqBody.flatMap { body => try { val tpe = - resolveType( + SchemaUtils.resolveType( body.schema, None, None, @@ -166,7 +132,7 @@ class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp val resBodyExpr = pathDef.resBody .flatMap { body => try { - val tpe = resolveType( + val tpe = SchemaUtils.resolveType( body.schema, None, None, @@ -200,12 +166,8 @@ class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp q"import java.util.UUID", q"import cats.effect.IO", q"import org.http4s.HttpRoutes", - q"import org.http4s.dsl.io._", - q"import org.http4s.circe.CirceEntityCodec.*", { - val importer = s"${config.basePackage}.models.*".parse[Importer].get - q"import ..${List(importer)}" - } - ) + q"import org.http4s.dsl.io._" + ) ++ frameworkModelImports ++ List(GenerationImports.modelWildcardImport(config.basePackage)) List( GeneratedFileSource( Paths.get(s"routes/${controllerName}.scala"), @@ -223,241 +185,6 @@ class Http4sGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp ) } - private def generateModelSources(namedSchemaDef: SchemaDefinition.Named, superType: Option[Type]): List[Stat] = { - val namedSchemaName = namedSchemaDef.name.capitalize - if (generatedNamedSchemas(namedSchemaName)) return List.empty - val typeName = Type.Name(namedSchemaName) - val termName = Term.Name(namedSchemaName) - val generatedModelSources = namedSchemaDef.schema match { - case obj: SchemaDefinition.Obj => - val params = obj.properties.flatMap { property => - try { - val propertyTpe = resolveType( - property.schema, - Some(property.name), - Some(namedSchemaName), - allowNullable = true, - context = s"${namedSchemaName}.${property.name}", - fallbackAnyType = t"Json" - ) - Some(param"${Term.Name(property.name)}: ${propertyTpe}") - } catch { - case e: UnsupportedSchemaException => - println(e.toString) - None - } - } - // enums defined in-place, we invent a new name for them.. - val adHocEnums = obj.properties.flatMap { property => - val enumValuesOpt = property.schema match { - case SchemaDefinition.Enum(values, _) => Some(values) - case SchemaDefinition.Opt(SchemaDefinition.Enum(values, _)) => Some(values) - case SchemaDefinition.Arr(SchemaDefinition.Enum(values, _), _, _, _) => Some(values) - case _ => None - } - enumValuesOpt - .map { values => - val adhocEnumName = generateEnumName(namedSchemaName, property.name) - val adhocEnumType = Type.Name(adhocEnumName) - val adhocEnumTerm = Term.Name(adhocEnumName) - val enumCaseDefs = Defn.RepeatedEnumCase( - List.empty, - values.map { enumDefCaseValue => - Term.Name(enumDefCaseValue) - } - ) - List( - q" enum ${adhocEnumType} { ${enumCaseDefs} } ", - q""" - object ${adhocEnumTerm} { - given Configuration = Configuration.default - given Codec[${adhocEnumType}] = ConfiguredEnumCodec.derived - } - """ - ) - } - .getOrElse(List.empty) - } - // validation - val validatorStmts = - List.empty[Stat] // generateValidatorStmts(typeName, obj.properties.map(p => (p.name, p.schema))) - val classDefinition: List[Stat] = superType match { - case Some(st) => - val extendsInit = init"${st}()" - List(q""" - case class ${typeName}( ..${Term.ParamClause(params)} ) extends ${extendsInit} - """) - case None => - List( - q" case class ${typeName}( ..${Term.ParamClause(params)} ) ", - q""" - object ${termName} { - given Configuration = Configuration.default - given Codec[${typeName}] = ConfiguredCodec.derived - } - """ - ) - } - val modelDefStats = classDefinition ++ - Option.when(validatorStmts.nonEmpty)(q""" object ${termName} { ..${validatorStmts} } """).toList - modelDefStats ++ adHocEnums - - case enumDef: SchemaDefinition.Enum => - val enumCaseDefs = Defn.RepeatedEnumCase( - List.empty, - enumDef.values.map { enumDefCaseValue => - Term.Name(enumDefCaseValue) - } - ) - List( - q" enum ${typeName} { ${enumCaseDefs} } ", - q""" - object ${termName} { - given Configuration = Configuration.default - given Codec[${typeName}] = ConfiguredEnumCodec.derived - } - """ - ) - case _: SchemaDefinition.Arr => - // TODO type alias ??? - List.empty - case oneOfSchema: SchemaDefinition.OneOf => - val oneOfCases = oneOfSchema.schemas.flatMap { - case SchemaDefinition.Ref(refName) => - openApiDefinition.namedSchemaDefinitions.defs.find(_.name == refName) match { - case Some(referencedNamedSchema) => generateModelSources(referencedNamedSchema, Some(typeName)) - case None => - println(s"Non-existing sub-schema type: '${refName}' [${namedSchemaName}}]") - None - } - case other => - println(s"Unsupported oneOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") - None - } - List( - q"sealed trait ${typeName} ", - q""" - object ${termName} { - given Configuration = Configuration.default.withDiscriminator(${Lit.String( - oneOfSchema.discriminatorPropertyName - )}) - given Codec[${typeName}] = ConfiguredCodec.derived - ..${oneOfCases} - } - """ - ) - } - generatedNamedSchemas += namedSchemaName - generatedModelSources - } - - private def resolveType( - schemaDef: SchemaDefinition, - propertyName: Option[String], - parentTypeName: Option[String], - // e.g. path enum cannot be null.. - allowNullable: Boolean, - context: String, - fallbackAnyType: Type - ): Type = schemaDef match { - case _: SchemaDefinition.Str => t"String" - case _: SchemaDefinition.Password => t"String" - case _: SchemaDefinition.Email => t"String" - case _: SchemaDefinition.Base64Bytes => t"String" // TODO use some kind of newtype.. ? - case _: SchemaDefinition.Int32 => t"Int" - case _: SchemaDefinition.Int64 => t"Long" - case _: SchemaDefinition.Num32 => t"Float" - case _: SchemaDefinition.Num64 => t"Double" - case _: SchemaDefinition.Bool => t"Boolean" - case _: SchemaDefinition.Uuid => t"UUID" - case _: SchemaDefinition.Date => t"LocalDate" - case _: SchemaDefinition.DateTime => t"Instant" - case SchemaDefinition.Opt(tpe) => - val coreTpe = - resolveType(tpe, propertyName, parentTypeName, allowNullable = allowNullable, context, fallbackAnyType) - if (allowNullable) t"Option[${coreTpe}]" - else coreTpe - case arr: SchemaDefinition.Arr => - val coreTpe = - resolveType(arr.schema, propertyName, parentTypeName, allowNullable = allowNullable, context, fallbackAnyType) - if (arr.uniqueItems) t"Set[${coreTpe}]" - else t"Seq[${coreTpe}]" - case SchemaDefinition.Enum(_, _) => - (parentTypeName.zip(propertyName)) match { - case Some((parentType, propName)) => - Type.Name(generateEnumName(parentType, propName)) - case _ => - throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for unnamed 'enum' [${context}]") - } - case SchemaDefinition.Ref(name) => Type.Name(name) - case SchemaDefinition.Named(name, _) => Type.Name(name) - case SchemaDefinition.Obj(_) => - throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for 'object' [${context}]") - case _: SchemaDefinition.OneOf => - throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for 'oneOf' [${context}]") - case _: SchemaDefinition.Unknown => fallbackAnyType - } - - // TODO use iron for refined types or smh - /* - private def generateValidatorStmts(typeName: Type, properties: List[(String, SchemaDefinition)]): List[Stat] = { - val validationCalls = properties.flatMap { case (name, schema) => - val propName = Term.Name(name) - schema match { - case int: SchemaDefinition.Int32 => - List( - int.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Int(min)) }, - int.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Int(max)) } - ).flatten - case long: SchemaDefinition.Int64 => - List( - long.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Long(min)) }, - long.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Long(max)) } - ).flatten - case float: SchemaDefinition.Num32 => - List( - float.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Float(min)) }, - float.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Float(max)) } - ).flatten - case double: SchemaDefinition.Num64 => - List( - double.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Double(min)) }, - double.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Double(max)) } - ).flatten - case str: SchemaDefinition.Str => - List( - str.minLength.map { min => "minLength" -> List(q"_.${propName}", Lit.Int(min)) }, - str.maxLength.map { max => "maxLength" -> List(q"_.${propName}", Lit.Int(max)) }, - str.pattern.map { pattern => "matches" -> List(q"_.${propName}", Lit.String(pattern)) } - ).flatten - case arr: SchemaDefinition.Arr => - List( - arr.minItems.map { min => "minItems" -> List(q"_.${propName}", Lit.Int(min)) }, - arr.maxItems.map { max => "maxItems" -> List(q"_.${propName}", Lit.Int(max)) } - ).flatten - case _ => List.empty - } - } - Option - .when(validationCalls.nonEmpty) { - val init = q"Validator.derived[${typeName}]" - val body = validationCalls.foldLeft(init: Term) { case (a, (funName, funArgs)) => - Term.Apply( - Term.Select(a, Term.Name(funName)), - Term.ArgClause(funArgs) - ) - } - q"given Validator[${typeName}] = ${body}" - } - .toList - }*/ - - private def generateEnumName(parentType: String, propName: String): String = { - val camelizedParentType = CaseUtils.toCamelCase(parentType, true, '_') - val camelizedPropName = CaseUtils.toCamelCase(propName, true, '_') - s"${camelizedParentType}${camelizedPropName}" - } - private def generatePkgSelect(pkg: String) = { val packageComponents = pkg.split("\\.").toList val firstSelect = q"${Term.Name(packageComponents(0))}.${Term.Name(packageComponents(1))}" diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/sharaf/SharafGenerator.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/sharaf/SharafGenerator.scala index cb8ec46..81e8a36 100644 --- a/openapi4s/src/main/scala/ba/sake/openapi4s/sharaf/SharafGenerator.scala +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/sharaf/SharafGenerator.scala @@ -7,53 +7,16 @@ import scala.meta.dialects.Scala34 import org.apache.commons.text.CaseUtils import ba.sake.regenesca._ import ba.sake.openapi4s.exceptions.UnsupportedSchemaException +import ba.sake.openapi4s.tupson.TupsonModelGenerator +import ba.sake.openapi4s.validson.ValidsonUtils -class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenApiDefinition) extends OpenApiGenerator { +class SharafGenerator( + config: OpenApiWriter.Config, + openApiDefinition: OpenApiDefinition, + frameworkModelImports: List[Import] +) extends OpenApiGenerator { - private val merger = SourceMerger(mergeDefBodies = true) - private val regenescaGenerator = RegenescaGenerator(merger) - - // keep track of done schemas (to avoid generating a subtype multiple times) - private var generatedNamedSchemas = Set.empty[String] - - override def generate(): Unit = { - println(s"Started generating Sharaf server for '${config.url}' OpenApi into '${config.baseFolder}' ...") - val packagePath = config.basePackage.replaceAll("\\.", "/") - val adaptedGenSourceFiles = generateSources.map { gsf => - gsf.copy(file = config.baseFolder.resolve(packagePath).resolve(gsf.file.toString)) - } - regenescaGenerator.generate(adaptedGenSourceFiles) - println(s"Finished generating Sharaf server for '${config.url}' OpenApi.") - } - - private[sharaf] def generateSources: Seq[GeneratedFileSource] = { - val modelsPkg = generatePkgSelect(s"${config.basePackage}.models") - val modelImports = List[Import]( - q"import java.time.*", - q"import java.util.UUID", - q"import org.typelevel.jawn.ast.JValue", - q"import ba.sake.tupson.*", - q"import ba.sake.validson.Validator" - ) - val modelFileSources = openApiDefinition.namedSchemaDefinitions.defs.flatMap { namedSchemaDef => - val namedSchemaName = namedSchemaDef.name.capitalize - val modelSources = generateModelSources(namedSchemaDef, None) - val allStmts = modelImports ++ modelSources - Option.when(modelSources.nonEmpty) { - GeneratedFileSource( - Paths.get(s"models/${namedSchemaName}.scala"), - source""" - // generated with OpenApi4s - package ${modelsPkg} { ..${allStmts} } - """ - ) - } - } - val controllerFileSources = generateControllersSources - modelFileSources ++ controllerFileSources - } - - private def generateControllersSources: List[GeneratedFileSource] = { + override def generate(): Seq[GeneratedFileSource] = { val groupedByTag = openApiDefinition.pathDefinitions.defs.groupBy(_.getTag) groupedByTag.flatMap { case (tag, pathDefinitions) => generateControllerSources(tag, pathDefinitions) @@ -71,7 +34,7 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp case PathSegment.Literal(value) => Lit.String(value) case PathSegment.Param(name, schema) => val tpe = - resolveType( + SchemaUtils.resolveType( schema, None, None, @@ -91,7 +54,7 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp .when(pathDef.queryParams.nonEmpty) { val (qpParams, adhocEnums) = pathDef.queryParams.flatMap { qp => val adhocEnumOpt = Option.when(qp.schema.isInstanceOf[SchemaDefinition.Enum]) { - val adhocEnumName = generateEnumName("QP", qp.name) + val adhocEnumName = SchemaUtils.generateEnumName("QP", qp.name) val adhocEnumType = Type.Name(adhocEnumName) val enumCaseDefs = Defn.RepeatedEnumCase( List.empty, @@ -105,7 +68,7 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp } val qpName = Name(qp.name) try { - val tpe = resolveType( + val tpe = SchemaUtils.resolveType( qp.schema, Some(qp.name), Some("QP"), @@ -124,7 +87,7 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp // validation // TODO figure out how to validate Option-al nicely val validatedQPs = pathDef.queryParams.filter(_.required).map(qp => (qp.name, qp.schema)) - val validatorStmts = generateValidatorStmts(t"QP", validatedQPs) + val validatorStmts = ValidsonUtils.generateStms(t"QP", validatedQPs) adhocEnums.flatten ++ List(q"case class QP(..${qpParams}) derives QueryStringRW") ++ Option.when(validatorStmts.nonEmpty)(q""" object QP { ..${validatorStmts} } """).toList ++ @@ -136,7 +99,7 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp val reqBodyStmts = pathDef.reqBody.flatMap { body => try { val tpe = - resolveType( + SchemaUtils.resolveType( body.schema, None, None, @@ -155,7 +118,7 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp val resBodyExpr = pathDef.resBody .flatMap { body => try { - val tpe = resolveType( + val tpe = SchemaUtils.resolveType( body.schema, None, None, @@ -186,11 +149,8 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp q"import sttp.model.StatusCode", q"import ba.sake.querson.QueryStringRW", q"import ba.sake.validson.Validator", - q"import ba.sake.sharaf.*, routing.*", { - val importer = s"${config.basePackage}.models.*".parse[Importer].get - q"import ..${List(importer)}" - } - ) + q"import ba.sake.sharaf.*, routing.*" + ) ++ frameworkModelImports ++ List(GenerationImports.modelWildcardImport(config.basePackage)) List( GeneratedFileSource( Paths.get(s"controllers/${controllerName}.scala"), @@ -208,224 +168,6 @@ class SharafGenerator(config: OpenApiGenerator.Config, openApiDefinition: OpenAp ) } - private def generateModelSources(namedSchemaDef: SchemaDefinition.Named, superType: Option[Type]): List[Stat] = { - val namedSchemaName = namedSchemaDef.name.capitalize - if (generatedNamedSchemas(namedSchemaName)) return List.empty - val typeName = Type.Name(namedSchemaName) - val termName = Term.Name(namedSchemaName) - val generatedModelSources = namedSchemaDef.schema match { - case obj: SchemaDefinition.Obj => - val params = obj.properties.flatMap { property => - try { - val propertyTpe = resolveType( - property.schema, - Some(property.name), - Some(namedSchemaName), - allowNullable = true, - context = s"${namedSchemaName}.${property.name}", - fallbackAnyType = t"JValue" - ) - Some(param"${Term.Name(property.name)}: ${propertyTpe}") - } catch { - case e: UnsupportedSchemaException => - println(e.toString) - None - } - } - // enums defined in-place, we invent a new name for them.. - val adHocEnums = obj.properties.flatMap { property => - val enumValuesOpt = property.schema match { - case SchemaDefinition.Enum(values, _) => Some(values) - case SchemaDefinition.Opt(SchemaDefinition.Enum(values, _)) => Some(values) - case SchemaDefinition.Arr(SchemaDefinition.Enum(values, _), _, _, _) => Some(values) - case _ => None - } - enumValuesOpt.flatMap { values => - val adhocEnumName = generateEnumName(namedSchemaName, property.name) - val adhocEnumType = Type.Name(adhocEnumName) - val enumCaseDefs = Defn.RepeatedEnumCase( - List.empty, - values.map { enumDefCaseValue => - Term.Name(enumDefCaseValue) - } - ) - Some( - q""" enum ${adhocEnumType} derives JsonRW { ${enumCaseDefs} }""" - ) - } - } - // validation - val validatorStmts = generateValidatorStmts(typeName, obj.properties.map(p => (p.name, p.schema))) - val classDefinition = superType match { - case Some(st) => - val extendsInit = init"${st}()" - q""" case class ${typeName}( ..${Term.ParamClause(params)} ) extends ${extendsInit}""" - case None => q""" case class ${typeName}( ..${Term.ParamClause(params)} ) derives JsonRW """ - } - val modelDefStats = List(classDefinition) ++ - Option.when(validatorStmts.nonEmpty)(q""" object ${termName} { ..${validatorStmts} } """).toList - - modelDefStats ++ adHocEnums - case enumDef: SchemaDefinition.Enum => - val enumCaseDefs = Defn.RepeatedEnumCase( - List.empty, - enumDef.values.map { enumDefCaseValue => - Term.Name(enumDefCaseValue) - } - ) - List( - q"""enum ${typeName} derives JsonRW { ${enumCaseDefs} } """ - ) - case _: SchemaDefinition.Arr => - // TODO type alias ??? - List.empty - case oneOfSchema: SchemaDefinition.OneOf => - val oneOfCases = oneOfSchema.schemas.flatMap { - case SchemaDefinition.Ref(refName) => - openApiDefinition.namedSchemaDefinitions.defs.find(_.name == refName) match { - case Some(referencedNamedSchema) => generateModelSources(referencedNamedSchema, Some(typeName)) - case None => - println(s"Non-existing sub-schema type: '${refName}' [${namedSchemaName}}]") - None - } - case other => - println(s"Unsupported oneOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") - None - } - List( - q""" - @discriminator(${Lit.String(oneOfSchema.discriminatorPropertyName)}) - sealed trait ${typeName} derives JsonRW - """, - q""" object ${termName} { ..${oneOfCases} } """ - ) - case allOfSchema: SchemaDefinition.AllOf => - val allOfCases: List[SchemaDefinition] = allOfSchema.schemas.flatMap { - case SchemaDefinition.Ref(refName) => - openApiDefinition.namedSchemaDefinitions.defs.find(_.name == refName).map(_.schema) - case obj: SchemaDefinition.Obj => Some(obj) - case other => - println(s"Unsupported allOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") - None - } - val mergedSchemasProps: List[SchemaProperty] = allOfCases.flatMap { - case SchemaDefinition.Obj(props) => props - case other => - println(s"Unsupported allOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") - List.empty - } - - generateModelSources(SchemaDefinition.Named(namedSchemaName, SchemaDefinition.Obj(mergedSchemasProps)), superType) - } - generatedNamedSchemas += namedSchemaName - generatedModelSources - } - - private def resolveType( - schemaDef: SchemaDefinition, - propertyName: Option[String], - parentTypeName: Option[String], - // e.g. path enum cannot be null.. - allowNullable: Boolean, - context: String, - fallbackAnyType: Type - ): Type = schemaDef match { - case _: SchemaDefinition.Str => t"String" - case _: SchemaDefinition.Password => t"String" - case _: SchemaDefinition.Email => t"String" - case _: SchemaDefinition.Base64Bytes => t"String" // TODO use some kind of newtype.. ? - case _: SchemaDefinition.Int32 => t"Int" - case _: SchemaDefinition.Int64 => t"Long" - case _: SchemaDefinition.Num32 => t"Float" - case _: SchemaDefinition.Num64 => t"Double" - case _: SchemaDefinition.Bool => t"Boolean" - case _: SchemaDefinition.Uuid => t"UUID" - case _: SchemaDefinition.Date => t"LocalDate" - case _: SchemaDefinition.DateTime => t"Instant" - case SchemaDefinition.Opt(tpe) => - val coreTpe = - resolveType(tpe, propertyName, parentTypeName, allowNullable = allowNullable, context, fallbackAnyType) - if (allowNullable) t"Option[${coreTpe}]" - else coreTpe - case arr: SchemaDefinition.Arr => - val coreTpe = - resolveType(arr.schema, propertyName, parentTypeName, allowNullable = allowNullable, context, fallbackAnyType) - if (arr.uniqueItems) t"Set[${coreTpe}]" - else t"Seq[${coreTpe}]" - case SchemaDefinition.Enum(_, _) => - (parentTypeName.zip(propertyName)) match { - case Some((parentType, propName)) => - Type.Name(generateEnumName(parentType, propName)) - case _ => - throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for unnamed 'enum' [${context}]") - } - case SchemaDefinition.Ref(name) => Type.Name(name) - case SchemaDefinition.Named(name, _) => Type.Name(name) - case SchemaDefinition.Obj(_) => - throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for 'object' [${context}]") - case _: SchemaDefinition.OneOf => - throw new UnsupportedSchemaException(s"Cannot make up an ad hoc type for 'oneOf' [${context}]") - case _: SchemaDefinition.Unknown => fallbackAnyType - } - - private def generateValidatorStmts(typeName: Type, properties: List[(String, SchemaDefinition)]): List[Stat] = { - val validationCalls = properties.flatMap { case (name, schema) => - val propName = Term.Name(name) - schema match { - case int: SchemaDefinition.Int32 => - List( - int.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Int(min)) }, - int.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Int(max)) } - ).flatten - case long: SchemaDefinition.Int64 => - List( - long.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Long(min)) }, - long.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Long(max)) } - ).flatten - case float: SchemaDefinition.Num32 => - List( - float.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Float(min)) }, - float.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Float(max)) } - ).flatten - case double: SchemaDefinition.Num64 => - List( - double.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Double(min)) }, - double.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Double(max)) } - ).flatten - case str: SchemaDefinition.Str => - List( - str.minLength.map { min => "minLength" -> List(q"_.${propName}", Lit.Int(min)) }, - str.maxLength.map { max => "maxLength" -> List(q"_.${propName}", Lit.Int(max)) }, - str.pattern.map { pattern => "matches" -> List(q"_.${propName}", Lit.String(pattern)) } - ).flatten - case arr: SchemaDefinition.Arr => - List( - arr.minItems.map { min => "minItems" -> List(q"_.${propName}", Lit.Int(min)) }, - arr.maxItems.map { max => "maxItems" -> List(q"_.${propName}", Lit.Int(max)) } - ).flatten - case _ => List.empty - } - } - Option - .when(validationCalls.nonEmpty) { - val init = q"Validator.derived[${typeName}]" - val body = validationCalls.foldLeft(init: Term) { case (a, (funName, funArgs)) => - Term.Apply( - Term.Select(a, Term.Name(funName)), - Term.ArgClause(funArgs) - ) - } - q"given Validator[${typeName}] = ${body}" - } - .toList - } - - private def generateEnumName(parentType: String, propName: String): String = { - val camelizedParentType = CaseUtils.toCamelCase(parentType, true, '_') - val camelizedPropName = CaseUtils.toCamelCase(propName, true, '_') - s"${camelizedParentType}${camelizedPropName}" - } - private def generatePkgSelect(pkg: String) = { val packageComponents = pkg.split("\\.").toList val firstSelect = q"${Term.Name(packageComponents(0))}.${Term.Name(packageComponents(1))}" diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/tupson/TupsonModelGenerator.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/tupson/TupsonModelGenerator.scala new file mode 100644 index 0000000..61b191b --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/tupson/TupsonModelGenerator.scala @@ -0,0 +1,166 @@ +package ba.sake.openapi4s +package tupson + +import java.nio.file.Paths +import scala.meta._ +import scala.meta.dialects.Scala34 +import ba.sake.regenesca._ +import ba.sake.openapi4s.exceptions.UnsupportedSchemaException +import ba.sake.openapi4s.validson.ValidsonUtils + +class TupsonModelGenerator(config: OpenApiWriter.Config, openApiDefinition: OpenApiDefinition) + extends OpenApiGenerator { + + // keep track of done schemas (to avoid generating a subtype multiple times) + private var generatedNamedSchemas = Set.empty[String] + + override def generate(): Seq[GeneratedFileSource] = { + val modelsPkg = generatePkgSelect(s"${config.basePackage}.models") + val modelImports = List[Import]( + q"import java.time.*", + q"import java.util.UUID", + q"import org.typelevel.jawn.ast.JValue", + q"import ba.sake.tupson.*", + q"import ba.sake.validson.Validator" + ) + val modelFileSources = openApiDefinition.namedSchemaDefinitions.defs.flatMap { namedSchemaDef => + val namedSchemaName = namedSchemaDef.name.capitalize + val modelSources = generateModelSources(namedSchemaDef, None) + val allStmts = modelImports ++ modelSources + Option.when(modelSources.nonEmpty) { + GeneratedFileSource( + Paths.get(s"models/${namedSchemaName}.scala"), + source""" + // generated with OpenApi4s + package ${modelsPkg} { ..${allStmts} } + """ + ) + } + } + modelFileSources + } + + def generateModelSources(namedSchemaDef: SchemaDefinition.Named, superType: Option[Type]): List[Stat] = { + val namedSchemaName = namedSchemaDef.name.capitalize + if (generatedNamedSchemas(namedSchemaName)) return List.empty + val typeName = Type.Name(namedSchemaName) + val termName = Term.Name(namedSchemaName) + val generatedModelSources = namedSchemaDef.schema match { + case obj: SchemaDefinition.Obj => + val params = obj.properties.flatMap { property => + try { + val propertyTpe = SchemaUtils.resolveType( + property.schema, + Some(property.name), + Some(namedSchemaName), + allowNullable = true, + context = s"${namedSchemaName}.${property.name}", + fallbackAnyType = t"JValue" + ) + Some(param"${Term.Name(property.name)}: ${propertyTpe}") + } catch { + case e: UnsupportedSchemaException => + println(e.toString) + None + } + } + // enums defined in-place, we invent a new name for them.. + val adHocEnums = obj.properties.flatMap { property => + val enumValuesOpt = property.schema match { + case SchemaDefinition.Enum(values, _) => Some(values) + case SchemaDefinition.Opt(SchemaDefinition.Enum(values, _)) => Some(values) + case SchemaDefinition.Arr(SchemaDefinition.Enum(values, _), _, _, _) => Some(values) + case _ => None + } + enumValuesOpt.flatMap { values => + val adhocEnumName = SchemaUtils.generateEnumName(namedSchemaName, property.name) + val adhocEnumType = Type.Name(adhocEnumName) + val enumCaseDefs = Defn.RepeatedEnumCase( + List.empty, + values.map { enumDefCaseValue => + Term.Name(enumDefCaseValue) + } + ) + Some( + q""" enum ${adhocEnumType} derives JsonRW { ${enumCaseDefs} }""" + ) + } + } + // validation + val validatorStmts = ValidsonUtils.generateStms(typeName, obj.properties.map(p => (p.name, p.schema))) + val classDefinition = superType match { + case Some(st) => + val extendsInit = init"${st}()" + q""" case class ${typeName}( ..${Term.ParamClause(params)} ) extends ${extendsInit}""" + case None => q""" case class ${typeName}( ..${Term.ParamClause(params)} ) derives JsonRW """ + } + val modelDefStats = List(classDefinition) ++ + Option.when(validatorStmts.nonEmpty)(q""" object ${termName} { ..${validatorStmts} } """).toList + + modelDefStats ++ adHocEnums + case enumDef: SchemaDefinition.Enum => + val enumCaseDefs = Defn.RepeatedEnumCase( + List.empty, + enumDef.values.map { enumDefCaseValue => + Term.Name(enumDefCaseValue) + } + ) + List( + q"""enum ${typeName} derives JsonRW { ${enumCaseDefs} } """ + ) + case _: SchemaDefinition.Arr => + // TODO type alias ??? + List.empty + case oneOfSchema: SchemaDefinition.OneOf => + val oneOfCases = oneOfSchema.schemas.flatMap { + case SchemaDefinition.Ref(refName) => + openApiDefinition.namedSchemaDefinitions.defs.find(_.name == refName) match { + case Some(referencedNamedSchema) => generateModelSources(referencedNamedSchema, Some(typeName)) + case None => + println(s"Non-existing sub-schema type: '${refName}' [${namedSchemaName}]") + None + } + case other => + println(s"Unsupported oneOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") + None + } + List( + q""" + @discriminator(${Lit.String(oneOfSchema.discriminatorPropertyName)}) + sealed trait ${typeName} derives JsonRW + """, + q""" object ${termName} { ..${oneOfCases} } """ + ) + case allOfSchema: SchemaDefinition.AllOf => + val allOfCases: List[SchemaDefinition] = allOfSchema.schemas.flatMap { + case SchemaDefinition.Ref(refName) => + openApiDefinition.namedSchemaDefinitions.defs.find(_.name == refName).map(_.schema) + case obj: SchemaDefinition.Obj => Some(obj) + case other => + println(s"Unsupported allOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") + None + } + val mergedSchemasProps: List[SchemaProperty] = allOfCases.flatMap { + case SchemaDefinition.Obj(props) => props + case other => + println(s"Unsupported allOf sub-schema type: '${other.getClass}' [${namedSchemaName}]") + List.empty + } + + generateModelSources( + SchemaDefinition.Named(namedSchemaName, SchemaDefinition.Obj(mergedSchemasProps)), + superType + ) + } + generatedNamedSchemas += namedSchemaName + generatedModelSources + } + + private def generatePkgSelect(pkg: String) = { + val packageComponents = pkg.split("\\.").toList + val firstSelect = q"${Term.Name(packageComponents(0))}.${Term.Name(packageComponents(1))}" + packageComponents.tail.tail.foldLeft(firstSelect) { (a, b) => + q"${a}.${Term.Name(b)}" + } + } +} diff --git a/openapi4s/src/main/scala/ba/sake/openapi4s/validson/ValidsonUtils.scala b/openapi4s/src/main/scala/ba/sake/openapi4s/validson/ValidsonUtils.scala new file mode 100644 index 0000000..01fa1d6 --- /dev/null +++ b/openapi4s/src/main/scala/ba/sake/openapi4s/validson/ValidsonUtils.scala @@ -0,0 +1,60 @@ +package ba.sake.openapi4s.validson + +import scala.meta._ +import scala.meta.dialects.Scala34 +import org.apache.commons.text.CaseUtils +import ba.sake.openapi4s.SchemaDefinition + +object ValidsonUtils { + def generateStms(typeName: Type, properties: List[(String, SchemaDefinition)]): List[Stat] = { + val validationCalls = properties.flatMap { case (name, schema) => + val propName = Term.Name(name) + schema match { + case int: SchemaDefinition.Int32 => + List( + int.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Int(min)) }, + int.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Int(max)) } + ).flatten + case long: SchemaDefinition.Int64 => + List( + long.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Long(min)) }, + long.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Long(max)) } + ).flatten + case float: SchemaDefinition.Num32 => + List( + float.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Float(min)) }, + float.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Float(max)) } + ).flatten + case double: SchemaDefinition.Num64 => + List( + double.minimum.map { min => "min" -> List(q"_.${propName}", Lit.Double(min)) }, + double.maximum.map { max => "max" -> List(q"_.${propName}", Lit.Double(max)) } + ).flatten + case str: SchemaDefinition.Str => + List( + str.minLength.map { min => "minLength" -> List(q"_.${propName}", Lit.Int(min)) }, + str.maxLength.map { max => "maxLength" -> List(q"_.${propName}", Lit.Int(max)) }, + str.pattern.map { pattern => "matches" -> List(q"_.${propName}", Lit.String(pattern)) } + ).flatten + case arr: SchemaDefinition.Arr => + List( + arr.minItems.map { min => "minItems" -> List(q"_.${propName}", Lit.Int(min)) }, + arr.maxItems.map { max => "maxItems" -> List(q"_.${propName}", Lit.Int(max)) } + ).flatten + case _ => List.empty + } + } + Option + .when(validationCalls.nonEmpty) { + val init = q"Validator.derived[${typeName}]" + val body = validationCalls.foldLeft(init: Term) { case (a, (funName, funArgs)) => + Term.Apply( + Term.Select(a, Term.Name(funName)), + Term.ArgClause(funArgs) + ) + } + q"given Validator[${typeName}] = ${body}" + } + .toList + } +} diff --git a/openapi4s/src/test/scala/ba/sake/openapi4s/OpenApiGeneratorSuite.scala b/openapi4s/src/test/scala/ba/sake/openapi4s/OpenApiGeneratorSuite.scala new file mode 100644 index 0000000..78bc1ef --- /dev/null +++ b/openapi4s/src/test/scala/ba/sake/openapi4s/OpenApiGeneratorSuite.scala @@ -0,0 +1,115 @@ +package ba.sake.openapi4s + +import java.nio.file.{Files, Path, Paths} +import scala.jdk.CollectionConverters._ + +class OpenApiGeneratorSuite extends munit.FunSuite { + + test("composed generator should support circe + http4s") { + val baseFolder = Files.createTempDirectory("openapi4s-circe-http4s") + val config = OpenApiWriter.Config( + url = TestUtils.getResourceUrl("petstore_3.0.0.json"), + baseFolder = baseFolder, + basePackage = "pkg", + models = "circe", + framework = "http4s" + ) + OpenApiWriter(config).write() + val generatedFiles = listScalaFiles(baseFolder.resolve("pkg")) + assert(generatedFiles.nonEmpty) + assert(generatedFiles.exists(_.startsWith("models/"))) + assert(generatedFiles.exists(_.startsWith("routes/"))) + assert(!generatedFiles.exists(_.startsWith("controllers/"))) + val routesFile = readGeneratedFile(baseFolder.resolve("pkg"), "routes/") + println(routesFile) + assert(routesFile.contains("import org.http4s.circe.CirceEntityCodec.*")) + } + + test("composed generator should support tupson + none") { + val baseFolder = Files.createTempDirectory("openapi4s-tupson-none") + val config = OpenApiWriter.Config( + url = TestUtils.getResourceUrl("petstore_3.0.0.json"), + baseFolder = baseFolder, + basePackage = "pkg", + models = "tupson", + framework = "none" + ) + OpenApiWriter(config).write() + val generatedFiles = listScalaFiles(baseFolder.resolve("pkg")) + assert(generatedFiles.nonEmpty) + assert(generatedFiles.exists(_.startsWith("models/"))) + assert(!generatedFiles.exists(_.startsWith("routes/"))) + assert(!generatedFiles.exists(_.startsWith("controllers/"))) + } + + test("composed generator should support none + http4s") { + val baseFolder = Files.createTempDirectory("openapi4s-none-http4s") + val config = OpenApiWriter.Config( + url = TestUtils.getResourceUrl("petstore_3.0.0.json"), + baseFolder = baseFolder, + basePackage = "pkg", + models = "none", + framework = "http4s" + ) + OpenApiWriter(config).write() + val generatedFiles = listScalaFiles(baseFolder.resolve("pkg")) + assert(generatedFiles.nonEmpty) + assert(!generatedFiles.exists(_.startsWith("models/"))) + assert(generatedFiles.exists(_.startsWith("routes/"))) + assert(!generatedFiles.exists(_.startsWith("controllers/"))) + val routesFile = readGeneratedFile(baseFolder.resolve("pkg"), "routes/") + assert(!routesFile.contains("import org.http4s.circe.CirceEntityCodec.*")) + } + + test("composed generator should reject none + none") { + interceptMessage[RuntimeException]("Invalid config: models=none and framework=none means nothing to generate.") { + OpenApiWriter( + OpenApiWriter.Config( + url = TestUtils.getResourceUrl("petstore_3.0.0.json"), + baseFolder = Paths.get("app"), + basePackage = "pkg", + models = "none", + framework = "none" + ) + ) + } + } + + test("composed generator should allow circe + sharaf") { + val baseFolder = Files.createTempDirectory("openapi4s-circe-sharaf") + val config = OpenApiWriter.Config( + url = TestUtils.getResourceUrl("petstore_3.0.0.json"), + baseFolder = baseFolder, + basePackage = "pkg", + models = "circe", + framework = "sharaf" + ) + OpenApiWriter(config).write() + val generatedFiles = listScalaFiles(baseFolder.resolve("pkg")) + assert(generatedFiles.nonEmpty) + assert(generatedFiles.exists(_.startsWith("models/"))) + assert(generatedFiles.exists(_.startsWith("controllers/"))) + } + + private def listScalaFiles(base: Path): List[String] = { + if (!Files.exists(base)) List.empty + else { + val stream = Files.walk(base) + try { + stream + .iterator() + .asScala + .filter(path => Files.isRegularFile(path) && path.getFileName.toString.endsWith(".scala")) + .map(path => base.relativize(path).toString.replace('\\', '/')) + .toList + } finally stream.close() + } + } + + private def readGeneratedFile(base: Path, prefix: String): String = { + val generatedFiles = listScalaFiles(base) + val relative = + generatedFiles.find(_.startsWith(prefix)).getOrElse(fail(s"Expected generated file with prefix '$prefix'")) + Files.readString(base.resolve(relative)) + } +} diff --git a/openapi4s/src/test/scala/ba/sake/openapi4s/http4s/Http4sGeneratorSuite.scala b/openapi4s/src/test/scala/ba/sake/openapi4s/http4s/Http4sGeneratorSuite.scala index becd1ba..51f8261 100644 --- a/openapi4s/src/test/scala/ba/sake/openapi4s/http4s/Http4sGeneratorSuite.scala +++ b/openapi4s/src/test/scala/ba/sake/openapi4s/http4s/Http4sGeneratorSuite.scala @@ -1,22 +1,28 @@ package ba.sake.openapi4s.http4s -import ba.sake.openapi4s._ - import java.nio.file.Paths +import ba.sake.openapi4s._ class Http4sGeneratorSuite extends munit.FunSuite { - test("generateSources should generate from petstore_3.0.0.json") { - val config = OpenApiGenerator.Config( + test("generate() should generate from petstore_3.0.0.json") { + val config = OpenApiWriter.Config( url = TestUtils.getResourceUrl("petstore_3.0.0.json"), baseFolder = Paths.get("app"), - basePackage = "pkg" + basePackage = "mypkg", + models = "circe", + framework = "http4s" ) val openapiDefinition = OpenApiDefinition.parse(config.url) - val generator = new Http4sGenerator(config, openapiDefinition) - val sources = generator.generateSources + val modelImportContract = ModelImportContracts.circe + val generator = new Http4sGenerator( + config, + openapiDefinition, + modelImportContract.frameworkImportsById(FrameworkBackendId.Http4s) + ) + val sources = generator.generate() println("*" * 100) - println("Generated Sharaf sources for petstore_3.0.0.json :") + println("Generated Http4s sources for petstore_3.0.0.json :") sources.foreach { source => println("*" * 50) print("*" * 20) @@ -26,17 +32,24 @@ class Http4sGeneratorSuite extends munit.FunSuite { } } - test("generateSources should generate from oneOf.yaml") { - val config = OpenApiGenerator.Config( + test("generate() should generate from oneOf.yaml") { + val config = OpenApiWriter.Config( url = TestUtils.getResourceUrl("oneOf.yaml"), baseFolder = Paths.get("app"), - basePackage = "pkg" + basePackage = "mypkg", + models = "circe", + framework = "http4s" ) val openapiDefinition = OpenApiDefinition.parse(config.url) - val generator = new Http4sGenerator(config, openapiDefinition) - val sources = generator.generateSources + val modelImportContract = ModelImportContracts.circe + val generator = new Http4sGenerator( + config, + openapiDefinition, + modelImportContract.frameworkImportsById(FrameworkBackendId.Http4s) + ) + val sources = generator.generate() println("*" * 100) - println("Generated Sharaf sources for oneOf.yaml :") + println("Generated Http4s sources for oneOf.yaml :") sources.foreach { source => println("*" * 50) print("*" * 20) diff --git a/openapi4s/src/test/scala/ba/sake/openapi4s/sharaf/SharafGeneratorSuite.scala b/openapi4s/src/test/scala/ba/sake/openapi4s/sharaf/SharafGeneratorSuite.scala index dba4979..9b85ffa 100644 --- a/openapi4s/src/test/scala/ba/sake/openapi4s/sharaf/SharafGeneratorSuite.scala +++ b/openapi4s/src/test/scala/ba/sake/openapi4s/sharaf/SharafGeneratorSuite.scala @@ -5,16 +5,22 @@ import ba.sake.openapi4s._ class SharafGeneratorSuite extends munit.FunSuite { - - test("generateSources should generate from petstore_3.0.0.json") { - val config = OpenApiGenerator.Config( + test("generate() should generate from petstore_3.0.0.json") { + val config = OpenApiWriter.Config( url = TestUtils.getResourceUrl("petstore_3.0.0.json"), baseFolder = Paths.get("app"), - basePackage = "pkg" + basePackage = "mypkg", + models = "tupson", + framework = "sharaf" ) val openapiDefinition = OpenApiDefinition.parse(config.url) - val generator = new SharafGenerator(config, openapiDefinition) - val sources = generator.generateSources + val modelImportContract = ModelImportContracts.tupson + val generator = new SharafGenerator( + config, + openapiDefinition, + modelImportContract.frameworkImportsById(FrameworkBackendId.Sharaf) + ) + val sources = generator.generate() println("*" * 100) println("Generated Sharaf sources for petstore_3.0.0.json :") sources.foreach { source => @@ -26,15 +32,22 @@ class SharafGeneratorSuite extends munit.FunSuite { } } - test("generateSources should generate from oneOf.yaml") { - val config = OpenApiGenerator.Config( + test("generate() should generate from oneOf.yaml") { + val config = OpenApiWriter.Config( url = TestUtils.getResourceUrl("oneOf.yaml"), baseFolder = Paths.get("app"), - basePackage = "pkg" + basePackage = "mypkg", + models = "tupson", + framework = "sharaf" ) val openapiDefinition = OpenApiDefinition.parse(config.url) - val generator = new SharafGenerator(config, openapiDefinition) - val sources = generator.generateSources + val modelImportContract = ModelImportContracts.tupson + val generator = new SharafGenerator( + config, + openapiDefinition, + modelImportContract.frameworkImportsById(FrameworkBackendId.Sharaf) + ) + val sources = generator.generate() println("*" * 100) println("Generated Sharaf sources for oneOf.yaml :") sources.foreach { source => @@ -46,15 +59,22 @@ class SharafGeneratorSuite extends munit.FunSuite { } } - test("generateSources should generate from spring_petclinic.yaml") { - val config = OpenApiGenerator.Config( + test("generate() should generate from spring_petclinic.yaml") { + val config = OpenApiWriter.Config( url = TestUtils.getResourceUrl("spring_petclinic.yaml"), baseFolder = Paths.get("app"), - basePackage = "pkg" + basePackage = "mypkg", + models = "tupson", + framework = "sharaf" ) val openapiDefinition = OpenApiDefinition.parse(config.url) - val generator = new SharafGenerator(config, openapiDefinition) - val sources = generator.generateSources + val modelImportContract = ModelImportContracts.tupson + val generator = new SharafGenerator( + config, + openapiDefinition, + modelImportContract.frameworkImportsById(FrameworkBackendId.Sharaf) + ) + val sources = generator.generate() println("*" * 100) println("Generated Sharaf sources for spring_petclinic.yaml :") sources.foreach { source =>