diff --git a/build.sc b/build.sc index 69ff979..8ec77cc 100644 --- a/build.sc +++ b/build.sc @@ -21,12 +21,35 @@ val scala212 = "2.12.18" val scala213 = "2.13.12" val scalaVersions = List(scala213, scala212) +object `compiler-core` extends Cross[CompilerCoreModule](scalaVersions) +trait CompilerCoreModule + extends CrossScalaModule + with BaseScalaModule + with BasePublishModule { + + def publishArtifactName = "smithytranslate-compiler-core" + + def moduleDeps = Seq(traits) + + def ivyDeps = Agg( + ivy"com.fasterxml.jackson.core:jackson-databind:2.15.3", + buildDeps.smithy.model, + buildDeps.smithy.build, + buildDeps.cats.mtl, + buildDeps.ciString, + buildDeps.slf4j, + buildDeps.alloy.core, + buildDeps.collectionsCompat + ) + +} + object `json-schema` extends Cross[JsonSchemaModule](scalaVersions) trait JsonSchemaModule extends CrossScalaModule with BaseScalaModule with BasePublishModule { - def moduleDeps = Seq(openapi()) + def moduleDeps = Seq(`compiler-core`()) def publishArtifactName = "smithytranslate-json-schema" @@ -52,19 +75,10 @@ trait OpenApiModule def publishArtifactName = "smithytranslate-openapi" - def ivyDeps = buildDeps.swagger.parser ++ Agg( - buildDeps.smithy.model, - buildDeps.smithy.build, - buildDeps.cats.mtl, - buildDeps.ciString, - buildDeps.slf4j, - buildDeps.alloy.core, - buildDeps.collectionsCompat - ) + def moduleDeps = Seq(`compiler-core`()) - def moduleDeps = Seq( - traits - ) + def ivyDeps = + buildDeps.swagger.parser object tests extends this.ScalaTests with BaseMunitTests { def ivyDeps = super.ivyDeps() ++ Agg( diff --git a/modules/cli/src/runners/FileUtils.scala b/modules/cli/src/runners/FileUtils.scala index 2493746..f0ec03c 100644 --- a/modules/cli/src/runners/FileUtils.scala +++ b/modules/cli/src/runners/FileUtils.scala @@ -17,23 +17,23 @@ package cli package runners import cats.data import cats.data.NonEmptyList +import smithytranslate.compiler.FileContents object FileUtils { def readAll( paths: NonEmptyList[os.Path], includedExtensions: List[String] - ): List[(data.NonEmptyList[String], String)] = { + ): List[FileContents] = { paths.toList.flatMap { path => if (os.isDir(path)) { val files = os .walk(path) .filter(p => includedExtensions.contains(p.ext)) files.map { in => - pathToNel(in, path) -> os - .read(in) + FileContents(pathToNel(in, path), os.read(in)) }.toList } else { - List((NonEmptyList.of(path.last), os.read(path))) + List(FileContents(NonEmptyList.of(path.last), os.read(path))) } } } diff --git a/modules/cli/src/runners/openapi/ParseAndCompile.scala b/modules/cli/src/runners/openapi/ParseAndCompile.scala index 8eb1451..683a94b 100644 --- a/modules/cli/src/runners/openapi/ParseAndCompile.scala +++ b/modules/cli/src/runners/openapi/ParseAndCompile.scala @@ -17,10 +17,14 @@ package smithytranslate.cli.runners.openapi import cats.data.NonEmptyList import smithytranslate.cli.runners.FileUtils.readAll -import smithytranslate.openapi.OpenApiCompiler +import smithytranslate.compiler.openapi.OpenApiCompiler import smithytranslate.cli.transformer.TranslateTransformer import software.amazon.smithy.model.Model -import smithytranslate.json_schema.JsonSchemaCompiler +import smithytranslate.compiler.json_schema.JsonSchemaCompiler +import smithytranslate.compiler.ToSmithyResult +import smithytranslate.compiler.ToSmithyCompilerOptions +import smithytranslate.compiler.openapi.OpenApiCompilerInput +import smithytranslate.compiler.json_schema.JsonSchemaCompilerInput object ParseAndCompile { def openapi( @@ -31,10 +35,12 @@ object ParseAndCompile { transformers: List[TranslateTransformer], useEnumTraitSyntax: Boolean, debug: Boolean - ): OpenApiCompiler.Result[Model] = { + ): ToSmithyResult[Model] = { val includedExtensions = List("yaml", "yml", "json") - val inputs = readAll(inputPaths, includedExtensions) - val opts = OpenApiCompiler.Options( + val input = OpenApiCompilerInput.UnparsedSpecs( + readAll(inputPaths, includedExtensions) + ) + val opts = ToSmithyCompilerOptions( useVerboseNames, validateInput, validateOutput, @@ -42,7 +48,7 @@ object ParseAndCompile { useEnumTraitSyntax, debug ) - OpenApiCompiler.parseAndCompile(opts, inputs: _*) + OpenApiCompiler.compile(opts, input) } def jsonSchema( @@ -53,10 +59,12 @@ object ParseAndCompile { transformers: List[TranslateTransformer], useEnumTraitSyntax: Boolean, debug: Boolean - ): OpenApiCompiler.Result[Model] = { + ): ToSmithyResult[Model] = { val includedExtensions = List("json") - val inputs = readAll(inputPaths, includedExtensions) - val opts = OpenApiCompiler.Options( + val input = JsonSchemaCompilerInput.UnparsedSpecs( + readAll(inputPaths, includedExtensions) + ) + val opts = ToSmithyCompilerOptions( useVerboseNames, validateInput, validateOutput, @@ -64,7 +72,7 @@ object ParseAndCompile { useEnumTraitSyntax, debug ) - JsonSchemaCompiler.parseAndCompile(opts, inputs: _*) + JsonSchemaCompiler.compile(opts, input) } } diff --git a/modules/cli/src/runners/openapi/ReportResult.scala b/modules/cli/src/runners/openapi/ReportResult.scala index b35803e..0175d19 100644 --- a/modules/cli/src/runners/openapi/ReportResult.scala +++ b/modules/cli/src/runners/openapi/ReportResult.scala @@ -15,13 +15,14 @@ package smithytranslate.cli.runners.openapi -import smithytranslate.openapi.OpenApiCompiler +import smithytranslate.compiler.openapi.OpenApiCompiler import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer import scala.jdk.CollectionConverters._ import software.amazon.smithy.build.transforms.FilterSuppressions import software.amazon.smithy.build.TransformContext import software.amazon.smithy.model.node.{Node, ObjectNode} +import smithytranslate.compiler.ToSmithyResult final case class ReportResult(outputPath: os.Path, outputJson: Boolean) { @@ -75,9 +76,9 @@ final case class ReportResult(outputPath: os.Path, outputJson: Boolean) { (path, os.Source.WritableSource(in._2)) } - def apply(result: OpenApiCompiler.Result[Model], debug: Boolean): Unit = { + def apply(result: ToSmithyResult[Model], debug: Boolean): Unit = { result match { - case OpenApiCompiler.Failure(error, modelErrors) => + case ToSmithyResult.Failure(error, modelErrors) => val message = if (modelErrors.isEmpty) { "An error occurred while importing your Open API resources." } else { @@ -92,7 +93,7 @@ final case class ReportResult(outputPath: os.Path, outputJson: Boolean) { } else { System.err.println(error.getMessage()) } - case OpenApiCompiler.Success(modelErrors, model) => + case ToSmithyResult.Success(modelErrors, model) => modelErrors.foreach(e => System.err.println(e.getMessage())) val smithyFiles = if (outputJson) getSmithyJsonFiles(model) else getSmithyFiles(model) diff --git a/modules/compiler-core/src/AbstractToSmithyCompiler.scala b/modules/compiler-core/src/AbstractToSmithyCompiler.scala new file mode 100644 index 0000000..e6c4ef3 --- /dev/null +++ b/modules/compiler-core/src/AbstractToSmithyCompiler.scala @@ -0,0 +1,105 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler + +import scala.jdk.OptionConverters._ +import software.amazon.smithy.model.{Model => SmithyModel} +import cats.syntax.all._ +import cats.data.Chain +import smithytranslate.compiler.internals.IModel +import smithytranslate.compiler.internals.IModelPostProcessor +import smithytranslate.compiler.internals.IModelToSmithy +import cats.data.NonEmptyList +import scala.jdk.CollectionConverters._ +import software.amazon.smithy.model.validation.Severity +import software.amazon.smithy.model.validation.ValidatedResult +import software.amazon.smithy.build.TransformContext + +/** Holds common logic that serves for the conversion of openapi/json-schema to smithy + */ +abstract class AbstractToSmithyCompiler[Input] protected[compiler] () { + + protected def convertToInternalModel( + opts: ToSmithyCompilerOptions, + input: Input + ): (Chain[ToSmithyError], IModel) + + def compile( + opts: ToSmithyCompilerOptions, + input: Input + ): ToSmithyResult[SmithyModel] = { + val (errors0, smithy0) = convertToInternalModel(opts, input) + .map(IModelPostProcessor(opts.useVerboseNames)) + .map(new IModelToSmithy(opts.useEnumTraitSyntax)) + val translationErrors = errors0.toList + + val assembled: ValidatedResult[SmithyModel] = validate(smithy0) + val validationEvents = if (opts.debug) { + assembled.getValidationEvents() + } else { + val errorEvents = assembled.getValidationEvents(Severity.ERROR) + val dangerEvents = assembled.getValidationEvents(Severity.DANGER) + val criticalErrors = new java.util.ArrayList(errorEvents) + criticalErrors.addAll(dangerEvents) + criticalErrors + } + val problematicSeverities = Set(Severity.DANGER, Severity.ERROR) + val hasProblematicEvents = validationEvents + .iterator() + .asScala + .map(_.getSeverity()) + .exists(problematicSeverities) + + val allErrors = + if (hasProblematicEvents) { + val smithyValidationFailed = + ToSmithyError.SmithyValidationFailed(validationEvents.asScala.toList) + smithyValidationFailed :: translationErrors + } else { + translationErrors + } + + val transformedModel = + assembled.getResult().toScala.map(transform(opts)) + + (transformedModel, NonEmptyList.fromList(allErrors)) match { + case (None, Some(nonEmptyErrors)) => + ToSmithyResult.Failure(nonEmptyErrors.head, nonEmptyErrors.tail) + case (None, None) => + ToSmithyResult.Failure( + ToSmithyError.SmithyValidationFailed(Nil), + translationErrors + ) + case (Some(model), None) => + ToSmithyResult.Success(translationErrors, model) + case (Some(model), Some(nonEmptyErrors)) => + if (opts.validateOutput) { + ToSmithyResult.Failure(nonEmptyErrors.head, nonEmptyErrors.tail) + } else { + ToSmithyResult.Success(allErrors, model) + } + } + } + + private def validate(model: SmithyModel): ValidatedResult[SmithyModel] = + SmithyModel.assembler().discoverModels().addModel(model).assemble() + + private def transform(opts: ToSmithyCompilerOptions)(model: SmithyModel): SmithyModel = + opts.transformers.foldLeft(model)((m, t) => + t.transform(TransformContext.builder().model(m).build()) + ) + +} diff --git a/modules/compiler-core/src/FileContents.scala b/modules/compiler-core/src/FileContents.scala new file mode 100644 index 0000000..7e9d567 --- /dev/null +++ b/modules/compiler-core/src/FileContents.scala @@ -0,0 +1,20 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler + +import cats.data.NonEmptyList + +final case class FileContents(path: NonEmptyList[String], content: String) diff --git a/modules/compiler-core/src/SmithyVersion.scala b/modules/compiler-core/src/SmithyVersion.scala new file mode 100644 index 0000000..fa41ea0 --- /dev/null +++ b/modules/compiler-core/src/SmithyVersion.scala @@ -0,0 +1,38 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler + +sealed abstract class SmithyVersion extends Product with Serializable { + override def toString(): String = this match { + case SmithyVersion.One => "1.0" + case SmithyVersion.Two => "2.0" + } +} +object SmithyVersion { + case object One extends SmithyVersion + case object Two extends SmithyVersion + + def fromString(string: String): Either[String, SmithyVersion] = { + val versionOne = Set("1", "1.0") + val versionTwo = Set("2", "2.0") + if (versionOne(string)) Right(SmithyVersion.One) + else if (versionTwo(string)) Right(SmithyVersion.Two) + else + Left( + s"expected one of ${versionOne ++ versionTwo}, but got '$string'" + ) + } +} diff --git a/modules/compiler-core/src/ToSmithyCompilerOptions.scala b/modules/compiler-core/src/ToSmithyCompilerOptions.scala new file mode 100644 index 0000000..1e35603 --- /dev/null +++ b/modules/compiler-core/src/ToSmithyCompilerOptions.scala @@ -0,0 +1,27 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler + +import software.amazon.smithy.build.ProjectionTransformer + +final case class ToSmithyCompilerOptions( + useVerboseNames: Boolean, + validateInput: Boolean, + validateOutput: Boolean, + transformers: List[ProjectionTransformer], + useEnumTraitSyntax: Boolean, + debug: Boolean +) diff --git a/modules/openapi/src/ModelError.scala b/modules/compiler-core/src/ToSmithyError.scala similarity index 72% rename from modules/openapi/src/ModelError.scala rename to modules/compiler-core/src/ToSmithyError.scala index 9069a20..ebecfc6 100644 --- a/modules/openapi/src/ModelError.scala +++ b/modules/compiler-core/src/ToSmithyError.scala @@ -13,41 +13,41 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler import scala.util.control.NoStackTrace import cats.data.NonEmptyChain import cats.syntax.all._ import software.amazon.smithy.model.validation.ValidationEvent -sealed trait ModelError extends Throwable +sealed trait ToSmithyError extends Throwable -object ModelError { +object ToSmithyError { - implicit val order: cats.Order[ModelError] = cats.Order.by(_.getMessage()) + implicit val order: cats.Order[ToSmithyError] = cats.Order.by(_.getMessage()) - case class Restriction(message: String) extends ModelError { + final case class Restriction(message: String) extends ToSmithyError { override def getMessage(): String = message } - case class ProcessingError(message: String) extends ModelError { + final case class ProcessingError(message: String) extends ToSmithyError { override def getMessage(): String = message } - case class SmithyValidationFailed( + final case class SmithyValidationFailed( smithyValidationEvents: List[ValidationEvent] - ) extends ModelError { + ) extends ToSmithyError { override def getMessage(): String = { s"Failed to validate the Smithy model:\n${smithyValidationEvents.mkString("\n")}" } } - case class BadRef(ref: String) extends ModelError + final case class BadRef(ref: String) extends ToSmithyError - case class OpenApiParseError( + final case class OpenApiParseError( namespace: NonEmptyChain[String], errorMessages: List[String] - ) extends ModelError + ) extends ToSmithyError with NoStackTrace { override def getMessage(): String = s"Unable to parse openapi file located at ${namespace.mkString_("/")} with errors: ${errorMessages diff --git a/modules/compiler-core/src/ToSmithyResult.scala b/modules/compiler-core/src/ToSmithyResult.scala new file mode 100644 index 0000000..68a485b --- /dev/null +++ b/modules/compiler-core/src/ToSmithyResult.scala @@ -0,0 +1,43 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler + +import cats.Functor + +// Either the smithy validation fails, in which case we get a left with +// the list of errors. Or smithy validation works and we get a pair of +// errors (that still pass smithy validation) and a smithy model +sealed trait ToSmithyResult[+A] + +object ToSmithyResult { + + final case class Failure[A](cause: Throwable, errors: List[ToSmithyError]) + extends ToSmithyResult[A] + final case class Success[A](error: List[ToSmithyError], value: A) + extends ToSmithyResult[A] + + implicit val functor: Functor[ToSmithyResult] = + new Functor[ToSmithyResult]() { + + override def map[A, B]( + fa: ToSmithyResult[A] + )(f: A => B): ToSmithyResult[B] = fa match { + case Failure(cause, errors) => Failure(cause, errors) + case Success(error, value) => Success(error, f(value)) + } + + } +} diff --git a/modules/openapi/src/internals/ApiKeyLocation.scala b/modules/compiler-core/src/internals/ApiKeyLocation.scala similarity index 79% rename from modules/openapi/src/internals/ApiKeyLocation.scala rename to modules/compiler-core/src/internals/ApiKeyLocation.scala index 4bb5deb..754ae5e 100644 --- a/modules/openapi/src/internals/ApiKeyLocation.scala +++ b/modules/compiler-core/src/internals/ApiKeyLocation.scala @@ -13,10 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals -sealed abstract class ApiKeyLocation extends Product with Serializable -object ApiKeyLocation { +private[compiler] sealed abstract class ApiKeyLocation + extends Product + with Serializable + +private[compiler] object ApiKeyLocation { case object Header extends ApiKeyLocation case object Query extends ApiKeyLocation } diff --git a/modules/openapi/src/internals/Context.scala b/modules/compiler-core/src/internals/Context.scala similarity index 66% rename from modules/openapi/src/internals/Context.scala rename to modules/compiler-core/src/internals/Context.scala index bf44bf9..ecdc9dd 100644 --- a/modules/openapi/src/internals/Context.scala +++ b/modules/compiler-core/src/internals/Context.scala @@ -13,29 +13,35 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals -import smithytranslate.openapi.ModelError - -case class Context(path: Name, hints: List[Hint], errors: List[ModelError]) { - def append(name: Name): Context = +private[compiler] case class Context( + path: Name, + hints: List[Hint], + errors: List[ToSmithyError] +) { + final def append(name: Name): Context = this.copy( path = path ++ name, hints = List.empty ) - def append(segment: Segment): Context = + final def append(segment: Segment): Context = this.copy( path = path :+ segment, hints = List.empty ) - def addHints(hints: List[Hint], retainTopLevel: Boolean = false): Context = + final def addHints( + hints: List[Hint], + retainTopLevel: Boolean = false + ): Context = this.copy(hints = this.hints.filter(_ != Hint.TopLevel || retainTopLevel) ++ hints ) - def addHints(hints: Hint*): Context = + final def addHints(hints: Hint*): Context = this.addHints(hints.toList) - def addErrors(errors: List[ModelError]): Context = + final def addErrors(errors: List[ToSmithyError]): Context = this.copy(errors = this.errors ++ errors) - def removeTopLevel(): Context = + final def removeTopLevel(): Context = this.copy(hints = this.hints.filter(_ != Hint.TopLevel)) } diff --git a/modules/openapi/src/internals/GetExtensions.scala b/modules/compiler-core/src/internals/GetExtensions.scala similarity index 91% rename from modules/openapi/src/internals/GetExtensions.scala rename to modules/compiler-core/src/internals/GetExtensions.scala index cb63ee3..310c661 100644 --- a/modules/openapi/src/internals/GetExtensions.scala +++ b/modules/compiler-core/src/internals/GetExtensions.scala @@ -13,25 +13,19 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals import software.amazon.smithy.model.node.Node import scala.jdk.CollectionConverters._ import scala.language.reflectiveCalls +import smithytranslate.compiler.internals._ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.{node => jackson} import java.time.format.DateTimeFormatter import java.time.ZoneId -object GetExtensions { - - def transformPattern[A]( - local: Local - ): OpenApiPattern[A] => OpenApiPattern[A] = { - val maybeHints = from(HasExtensions.unsafeFrom(local.schema)) - (pattern: OpenApiPattern[A]) => - pattern.mapContext(_.addHints(maybeHints, retainTopLevel = true)) - } +private[compiler] object GetExtensions { // Using reflective calls because openapi does not seem to have a common interface // that exposes the presence of extensions. diff --git a/modules/openapi/src/internals/Hint.scala b/modules/compiler-core/src/internals/Hint.scala similarity index 93% rename from modules/openapi/src/internals/Hint.scala rename to modules/compiler-core/src/internals/Hint.scala index 42e33df..23d753b 100644 --- a/modules/openapi/src/internals/Hint.scala +++ b/modules/compiler-core/src/internals/Hint.scala @@ -13,12 +13,12 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals import software.amazon.smithy.model.node.Node // format: off -sealed abstract class Hint extends Product with Serializable -object Hint { +private[compiler] sealed abstract class Hint extends Product with Serializable +private[compiler] object Hint { case class Header(name: String) extends Hint case class Length(min: Option[Long], max: Option[Long]) extends Hint case class Range(min: Option[BigDecimal], max: Option[BigDecimal]) extends Hint diff --git a/modules/openapi/src/internals/HttpMethod.scala b/modules/compiler-core/src/internals/HttpMethod.scala similarity index 85% rename from modules/openapi/src/internals/HttpMethod.scala rename to modules/compiler-core/src/internals/HttpMethod.scala index fb12372..e15fc79 100644 --- a/modules/openapi/src/internals/HttpMethod.scala +++ b/modules/compiler-core/src/internals/HttpMethod.scala @@ -13,9 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals -sealed abstract class HttpMethod extends Product with Serializable -object HttpMethod { +package smithytranslate.compiler.internals + +private[compiler] sealed abstract class HttpMethod + extends Product + with Serializable + +private[compiler] object HttpMethod { case object GET extends HttpMethod case object POST extends HttpMethod case object PUT extends HttpMethod diff --git a/modules/openapi/src/internals/IModel.scala b/modules/compiler-core/src/internals/IModel.scala similarity index 81% rename from modules/openapi/src/internals/IModel.scala rename to modules/compiler-core/src/internals/IModel.scala index 94ea0a5..156e59d 100644 --- a/modules/openapi/src/internals/IModel.scala +++ b/modules/compiler-core/src/internals/IModel.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals import cats.kernel.Monoid import cats.syntax.all._ @@ -25,12 +25,12 @@ import cats.Show * what is eventually converted to smithy, with additional contextual * information. */ -case class IModel( +private[compiler] case class IModel( definitions: Vector[Definition], suppressions: Vector[Suppression] ) -object IModel { +private[compiler] object IModel { val empty = IModel(Vector.empty, Vector.empty) @@ -43,14 +43,18 @@ object IModel { } -final case class Suppression(id: String, namespace: Namespace, reason: String) +private[compiler] final case class Suppression( + id: String, + namespace: Namespace, + reason: String +) /** Represents anything that can be directly reference, whether shape or member * : * * * Structure * Union * Newtype * Field * Alternative * Enumeration */ -sealed abstract class Definition { +private[compiler] sealed abstract class Definition { def modelType: String def id: DefId def hints: List[Hint] @@ -70,12 +74,12 @@ sealed abstract class Definition { /** Self explanatory, represents a package, a folder in the filesystem, a path * in a more general URI ... */ -case class Namespace(segments: List[String]) { +private[compiler] case class Namespace(segments: List[String]) { def show: String = segments.mkString(".") override def toString: String = show } -sealed trait Segment { self => +private[compiler] sealed trait Segment { self => def value: CIString def mapValue(f: CIString => CIString): Segment = new Segment { def value: CIString = f(self.value) @@ -85,14 +89,16 @@ sealed trait Segment { self => case _ => false } } -object Segment { +private[compiler] object Segment { final case class Arbitrary(value: CIString) extends Segment final case class Derived(value: CIString) extends Segment final case class StandardLib(value: CIString) extends Segment implicit val show: Show[Segment] = _.value.toString } -final case class Name(segments: NonEmptyChain[Segment]) { +private[compiler] final case class Name( + segments: NonEmptyChain[Segment] +) { def ++(that: Name): Name = this.copy(segments = this.segments ++ that.segments) def :+(s: Segment): Name = this.copy(segments = this.segments :+ s) @@ -102,7 +108,7 @@ final case class Name(segments: NonEmptyChain[Segment]) { s"#$parts" } } -object Name { +private[compiler] object Name { def apply(head: Segment, tail: Segment*): Name = new Name( NonEmptyChain(head, tail: _*) ) @@ -114,7 +120,7 @@ object Name { /** An identifier, composed of a namespace and a name. Can be called directly * from */ -abstract class Id { +private[compiler] abstract class Id { def namespace: Namespace def name: Name } @@ -122,7 +128,7 @@ abstract class Id { /** An identifier that specifically resolves to a shape (ie must not resolve to * a member) */ -sealed case class DefId( +private[compiler] sealed case class DefId( namespace: Namespace, name: Name ) extends Id { @@ -135,7 +141,7 @@ sealed case class DefId( /** An identifier that specifically resolves to a member (ie must not resolve to * a shape) */ -case class MemberId( +private[compiler] case class MemberId( modelId: DefId, memberName: Segment ) extends Id { @@ -150,7 +156,8 @@ case class MemberId( /** Represents datatype that have an idea (as opposed to primitives and * collections) */ -sealed abstract class Shape(val shapeType: String) extends Definition { +private[compiler] sealed abstract class Shape(val shapeType: String) + extends Definition { def id: DefId def modelType: String = shapeType } @@ -158,13 +165,13 @@ sealed abstract class Shape(val shapeType: String) extends Definition { /** Represents "members" of another type. A member of a Structure is a Field, a * member of a union is an Alternative. */ -sealed abstract class Member(val memberType: String) { +private[compiler] sealed abstract class Member(val memberType: String) { def modelType = memberType } /** A member of a structure. */ -case class Field( +private[compiler] case class Field( id: MemberId, tpe: DefId, hints: List[Hint] @@ -174,7 +181,7 @@ case class Field( /** A member of a union. */ -case class Alternative( +private[compiler] case class Alternative( id: MemberId, tpe: DefId, hints: List[Hint] @@ -182,26 +189,26 @@ case class Alternative( /** Aka product, structure, object, case-class, etc. */ -case class Structure( +private[compiler] case class Structure( id: DefId, localFields: Vector[Field], parents: Vector[DefId], hints: List[Hint] ) extends Shape("Structure") -case class SetDef( +private[compiler] case class SetDef( id: DefId, member: DefId, hints: List[Hint] = Nil ) extends Shape("Set") -case class ListDef( +private[compiler] case class ListDef( id: DefId, member: DefId, hints: List[Hint] = Nil ) extends Shape("List") -case class MapDef( +private[compiler] case class MapDef( id: DefId, key: DefId, value: DefId, @@ -210,7 +217,7 @@ case class MapDef( /** Aka sums, coproducts, oneOfs, sealed-traits, etc. */ -case class Union( +private[compiler] case class Union( id: DefId, alts: Vector[Alternative], kind: UnionKind, @@ -219,18 +226,21 @@ case class Union( /** Semantic type alias. */ -case class Newtype(id: DefId, target: DefId, hints: List[Hint] = Nil) - extends Shape("Newtype") +private[compiler] case class Newtype( + id: DefId, + target: DefId, + hints: List[Hint] = Nil +) extends Shape("Newtype") /** Stringly enumeration. */ -case class Enumeration( +private[compiler] case class Enumeration( id: DefId, values: Vector[String], hints: List[Hint] = Nil ) extends Shape("Enumeration") -case class OperationDef( +private[compiler] case class OperationDef( id: DefId, input: Option[DefId], output: Option[DefId], @@ -238,7 +248,7 @@ case class OperationDef( hints: List[Hint] = Nil ) extends Shape("Operation") -case class ServiceDef( +private[compiler] case class ServiceDef( id: DefId, operations: Vector[DefId], hints: List[Hint] = Nil diff --git a/modules/openapi/src/internals/IModelPostProcessor.scala b/modules/compiler-core/src/internals/IModelPostProcessor.scala similarity index 90% rename from modules/openapi/src/internals/IModelPostProcessor.scala rename to modules/compiler-core/src/internals/IModelPostProcessor.scala index 49b0f06..5ea8d2a 100644 --- a/modules/openapi/src/internals/IModelPostProcessor.scala +++ b/modules/compiler-core/src/internals/IModelPostProcessor.scala @@ -13,13 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals import postprocess._ -trait IModelPostProcessor extends (IModel => IModel) +private[compiler] trait IModelPostProcessor extends (IModel => IModel) -object IModelPostProcessor { +private[compiler] object IModelPostProcessor { private[this] val defaultTransformers: List[IModelPostProcessor] = List( ExternalMemberRefTransformer, NewtypeTransformer, diff --git a/modules/openapi/src/internals/IModelToSmithy.scala b/modules/compiler-core/src/internals/IModelToSmithy.scala similarity index 97% rename from modules/openapi/src/internals/IModelToSmithy.scala rename to modules/compiler-core/src/internals/IModelToSmithy.scala index 48c1a41..703af1a 100644 --- a/modules/openapi/src/internals/IModelToSmithy.scala +++ b/modules/compiler-core/src/internals/IModelToSmithy.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals import alloy.DateFormatTrait import alloy.DiscriminatedUnionTrait @@ -27,10 +27,10 @@ import smithytranslate.ContentTypeDiscriminatedTrait import smithytranslate.ContentTypeTrait import smithytranslate.ErrorMessageTrait import smithytranslate.NullFormatTrait -import smithytranslate.openapi.internals.Hint.Header -import smithytranslate.openapi.internals.Hint.QueryParam -import smithytranslate.openapi.internals.TimestampFormat.DateTime -import smithytranslate.openapi.internals.TimestampFormat.SimpleDate +import smithytranslate.compiler.internals.Hint.Header +import smithytranslate.compiler.internals.Hint.QueryParam +import smithytranslate.compiler.internals.TimestampFormat.DateTime +import smithytranslate.compiler.internals.TimestampFormat.SimpleDate import software.amazon.smithy.model.Model import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.pattern.UriPattern @@ -41,7 +41,7 @@ import software.amazon.smithy.model.shapes.{Shape => JShape} import scala.jdk.CollectionConverters._ -final class IModelToSmithy(useEnumTraitSyntax: Boolean) +private[compiler] final class IModelToSmithy(useEnumTraitSyntax: Boolean) extends (IModel => Model) { def apply(iModel: IModel): Model = { @@ -399,7 +399,7 @@ final class IModelToSmithy(useEnumTraitSyntax: Boolean) implicit class ShapeBuilderOps[A <: AbstractShapeBuilder[A, S], S <: JShape]( builder: AbstractShapeBuilder[A, S] ) { - def addHints(hints: List[Hint]): A = { + final def addHints(hints: List[Hint]): A = { hintsToTraits(hints).foreach(builder.addTrait) builder.asInstanceOf[A] } diff --git a/modules/compiler-core/src/internals/NonEmptySegments.scala b/modules/compiler-core/src/internals/NonEmptySegments.scala new file mode 100644 index 0000000..4a95b08 --- /dev/null +++ b/modules/compiler-core/src/internals/NonEmptySegments.scala @@ -0,0 +1,24 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler +package internals + +private[compiler] object NonEmptySegments { + def unapply(s: String): Option[(List[String], String)] = { + val segments = s.split("/").toList.filterNot(_.isEmpty()) + segments.lastOption.map(last => segments.dropRight(1) -> last) + } +} diff --git a/modules/openapi/src/internals/OpenApiPattern.scala b/modules/compiler-core/src/internals/OpenApiPattern.scala similarity index 95% rename from modules/openapi/src/internals/OpenApiPattern.scala rename to modules/compiler-core/src/internals/OpenApiPattern.scala index 01513dd..1494f46 100644 --- a/modules/openapi/src/internals/OpenApiPattern.scala +++ b/modules/compiler-core/src/internals/OpenApiPattern.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler package internals import cats.{Applicative, Traverse} @@ -22,7 +22,7 @@ import cats.Eval import OpenApiPattern.HintedAlternative // format: off -sealed trait OpenApiPattern[+A] { +private[compiler] sealed trait OpenApiPattern[+A] { def context: Context def mapContext(f: Context => Context) : OpenApiPattern[A] = this match { @@ -63,7 +63,7 @@ sealed trait OpenApiPattern[+A] { } } -object OpenApiPattern { +private[compiler] object OpenApiPattern { type HintedAlternative[A] = (List[Hint], A) @@ -90,7 +90,7 @@ object OpenApiPattern { case class OpenApiPrimitive(context: Context, tpe: Primitive) extends OpenApiPattern[Nothing] case class OpenApiRef(context: Context, ref: DefId) extends OpenApiPattern[Nothing] case class OpenApiEnum(context: Context, values: Vector[String]) extends OpenApiPattern[Nothing] - case class OpenApiShortStop(context: Context, error: ModelError) extends OpenApiPattern[Nothing] + case class OpenApiShortStop(context: Context, error: ToSmithyError) extends OpenApiPattern[Nothing] case class OpenApiNull(context: Context) extends OpenApiPattern[Nothing] case class OpenApiMap[A](context: Context, items: A) extends OpenApiPattern[A] case class OpenApiArray[A](context: Context, items: A) extends OpenApiPattern[A] diff --git a/modules/compiler-core/src/internals/PatternFolder.scala b/modules/compiler-core/src/internals/PatternFolder.scala new file mode 100644 index 0000000..d305ded --- /dev/null +++ b/modules/compiler-core/src/internals/PatternFolder.scala @@ -0,0 +1,190 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler +package internals + +import cats.Monad +import cats.Parallel +import cats.mtl._ +import cats.data._ +import Primitive._ +import cats.syntax.all._ +import org.typelevel.ci.CIString + +private[compiler] final class PatternFolder[F[ + _ +]: Parallel: TellShape: TellError]( + namespace: Path +) { + implicit val F: Monad[F] = Parallel[F].monad + + def id(context: Context): DefId = { + DefId(Namespace(namespace.toList), context.path) + } + + def errorId(context: Context): DefId = { + DefId(errorNamespace, context.path) + } + + private def errorNamespace: Namespace = Namespace(List("error")) + + private def recordError(e: ToSmithyError): F[Unit] = + Tell.tell(Chain.one(e)) + + private def recordDef(definition: Definition): F[Unit] = + Tell.tell(Chain.one(Right(definition))) + + def std(name: String, hints: Hint*) = + ( + DefId( + Namespace(List("smithy", "api")), + Name.stdLib(name) + ), + hints.toList + ) + def smithyTranslate(name: String, hints: Hint*) = + ( + DefId(Namespace(List("smithytranslate")), Name.stdLib(name)), + hints.toList + ) + def alloy(name: String, hints: Hint*) = + ( + DefId(Namespace(List("alloy")), Name.stdLib(name)), + hints.toList + ) + + // format: off + def idFromPrimitive(primitive: Primitive): (DefId, List[Hint]) = + primitive match { + case PInt => std("Integer") + case PBoolean => std("Boolean") + case PString => std("String") + case PLong => std("Long") + case PByte => std("Byte") + case PFloat => std("Float") + case PDouble => std("Double") + case PShort => std("Short") + case PBytes => std("Blob") + case PFreeForm => std("Document") + case PUUID => alloy("UUID") + case PDate => std("String", Hint.Timestamp(TimestampFormat.SimpleDate)) + case PDateTime => std("Timestamp", Hint.Timestamp(TimestampFormat.DateTime)) + case PTimestamp => std("Timestamp") + } + // format: on + + /** Folds one layer into a type, recording definitions into the monadic tell + * as we go + */ + def fold(layer: OpenApiPattern[DefId]): F[DefId] = { + layer.context.errors.traverse(recordError) *> + (layer match { + case OpenApiPrimitive(context, primitive) => + val ntId = id(context) + val (target, hints) = idFromPrimitive(primitive) + val nt = Newtype(ntId, target, context.hints ++ hints) + recordDef(nt).as(ntId) + + case OpenApiRef(context, target) => + val ntId = id(context) + val nt = Newtype(ntId, target, context.hints) + recordDef(nt).as(ntId) + + case OpenApiEnum(context, values) => + val defId = id(context) + recordDef( + Enumeration(defId, values.filterNot(_ == null), context.hints) + ).as(defId) + + case OpenApiNull(context) => + val ntId = id(context) + val target = + DefId(Namespace(List("smithytranslate")), Name.stdLib("Null")) + val nt = Newtype(ntId, target, context.hints) + recordDef(nt).as(ntId) + + case OpenApiMap(context, itemType) => + val defId = id(context) + val (key, _) = idFromPrimitive(Primitive.PString) + val definition = MapDef(defId, key, itemType, context.hints) + recordDef(definition).as(defId) + + case OpenApiArray(context, itemType) => + val defId = id(context) + recordDef(ListDef(defId, itemType, context.hints)).as(defId) + + case OpenApiSet(context, itemType) => + val defId = id(context) + recordDef(SetDef(defId, itemType, context.hints)).as(defId) + + case OpenApiAllOf(context, parentTypes) => + val shapeId = id(context) + F.pure(Structure(shapeId, Vector.empty, parentTypes, context.hints)) + .flatTap(recordDef) + .as(shapeId) + + case OpenApiOneOf(context, alternatives, unionKind) => + val shapeId = id(context) + alternatives + .parTraverse { case (hints, tpe @ DefId(_, name)) => + val n = hints + .collectFirst { case Hint.ContentTypeLabel(l) => + StringUtil.toCamelCase(l) + } + .getOrElse(name.segments.last.value) + val altId = + MemberId(shapeId, Segment.Derived(CIString(n.toString))) + val alt = Alternative(altId, tpe, hints) + F.pure(alt) + } + .flatTap(alts => + recordDef(Union(shapeId, alts, unionKind, context.hints)) + ) + .as(shapeId) + + case OpenApiObject(context, items) => + val shapeId = id(context) + items + .map { case ((name, required), tpe) => + val fieldId = MemberId(shapeId, Segment.Derived(CIString(name))) + val hints = if (required) List(Hint.Required) else List.empty + Field(fieldId, tpe, hints) + } + .pure[F] + .flatTap { fields => + recordDef( + Structure(shapeId, fields, Vector.empty, context.hints) + ) + } + .as(shapeId) + + case OpenApiShortStop(context, error) => + // When an error was encountered during the unfold, we materialise it + // as an empty shape in the `error` namespace + val shapeId = errorId(context) + val definition = + Structure( + shapeId, + Vector.empty, + Vector.empty, + context.hints :+ Hint.ErrorMessage(error.getMessage) + ) + recordError(error) *> + recordDef(definition) *> + F.pure(shapeId) + }) + } +} diff --git a/modules/openapi/src/internals/Primitive.scala b/modules/compiler-core/src/internals/Primitive.scala similarity index 92% rename from modules/openapi/src/internals/Primitive.scala rename to modules/compiler-core/src/internals/Primitive.scala index 7a24cb2..60a375b 100644 --- a/modules/openapi/src/internals/Primitive.scala +++ b/modules/compiler-core/src/internals/Primitive.scala @@ -13,17 +13,18 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals + import java.util.Date import java.time.OffsetDateTime import java.time.Instant import java.util.UUID -sealed trait Primitive { +private[compiler] sealed trait Primitive { type T } -object Primitive { +private[compiler] object Primitive { type Aux[TT] = Primitive { type T = TT } diff --git a/modules/compiler-core/src/internals/RefParser.scala b/modules/compiler-core/src/internals/RefParser.scala new file mode 100644 index 0000000..b51c8ed --- /dev/null +++ b/modules/compiler-core/src/internals/RefParser.scala @@ -0,0 +1,99 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler +package internals + +import cats.data.NonEmptyChain +import org.typelevel.ci.CIString +import cats.syntax.all._ + +/* + * The most complicated thing + */ +private[compiler] abstract class RefParser(ns: Path) { + + private def handleRef( + uri: java.net.URI, + pathSegsIn: List[String], + fileName: String, + segments: Option[List[String]], + last: Option[String] + ): Either[ToSmithyError, DefId] = { + val pathSegs = pathSegsIn.dropWhile(_ == ".") + + val ups = pathSegs.takeWhile(_ == "..").size + if (ns.length < ups + 1) { + // namespace contains the name of the file + val error = ToSmithyError.Restriction(s"Ref $uri goes too far up") + Left(error) + } else { + // Removing as many ".." as needed to standardise the namespace + val nsPrefix = ns.toChain.toList.dropRight(ups + 1) + val nsPrefix2 = pathSegs.drop(ups) + val splitName = fileName.split('.') + val nsLastPart = + if (splitName.size > 1) splitName.dropRight(1).mkString(".") + else fileName + val fullNs = + Namespace((nsPrefix ++ nsPrefix2 :+ nsLastPart)) + val name = (segments, last) match { + case (Some(seg), Some(l)) => + NonEmptyChain + .fromSeq(seg.map(s => Segment.Arbitrary(CIString(s)))) + .map(Name(_)) + .map(_ ++ Name.derived(l)) + .getOrElse(Name.derived(l)) + case _ => Name.derived(nsLastPart) + } + Right(DefId(fullNs, name)) + } + } + + def apply(ref: String): Either[ToSmithyError, DefId] = + scala.util + .Try(java.net.URI.create(ref)) + .toEither + .leftMap(_ => ToSmithyError.BadRef(ref)) + .flatMap(uri => + Option(uri.getScheme()) match { + case Some("file") => Right(uri) + case None => Right(uri) + case Some(_) => Left(ToSmithyError.BadRef(ref)) + } + ) + .flatMap { uri => + (uri.getPath(), uri.getFragment()) match { + case ("", NonEmptySegments(segments, last)) => + val n = Namespace(ns.toChain.toList) + val name = + NonEmptyChain + .fromSeq( + segments.map(s => Segment.Arbitrary(CIString(s))) + ) + .map(Name(_)) + .map(_ ++ Name.derived(last)) + .getOrElse(Name.derived(last)) + Right(DefId(n, name)) + case (NonEmptySegments(pathSegsIn, fileName), null) => + handleRef(uri, pathSegsIn, fileName, None, None) + case ( + NonEmptySegments(pathSegsIn, fileName), + NonEmptySegments(segments, last) + ) => + handleRef(uri, pathSegsIn, fileName, Some(segments), Some(last)) + } + } +} diff --git a/modules/openapi/src/internals/SecurityScheme.scala b/modules/compiler-core/src/internals/SecurityScheme.scala similarity index 82% rename from modules/openapi/src/internals/SecurityScheme.scala rename to modules/compiler-core/src/internals/SecurityScheme.scala index 3e3c8e3..8a35006 100644 --- a/modules/openapi/src/internals/SecurityScheme.scala +++ b/modules/compiler-core/src/internals/SecurityScheme.scala @@ -13,10 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals -sealed abstract class SecurityScheme extends Product with Serializable -object SecurityScheme { +private[compiler] sealed abstract class SecurityScheme + extends Product + with Serializable + +private[compiler] object SecurityScheme { final case class ApiKey(location: ApiKeyLocation, name: String) extends SecurityScheme case object BasicAuth extends SecurityScheme diff --git a/modules/openapi/src/internals/StringUtil.scala b/modules/compiler-core/src/internals/StringUtil.scala similarity index 92% rename from modules/openapi/src/internals/StringUtil.scala rename to modules/compiler-core/src/internals/StringUtil.scala index 6db8984..1c13c6d 100644 --- a/modules/openapi/src/internals/StringUtil.scala +++ b/modules/compiler-core/src/internals/StringUtil.scala @@ -13,9 +13,10 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals + +private[internals] object StringUtil { -object StringUtil { def toCamelCase(value: String): String = { val (_, output) = value.foldLeft((false, "")) { case ((wasLastSkipped, str), c) => @@ -29,4 +30,5 @@ object StringUtil { } output } + } diff --git a/modules/openapi/src/internals/TimestampFormat.scala b/modules/compiler-core/src/internals/TimestampFormat.scala similarity index 80% rename from modules/openapi/src/internals/TimestampFormat.scala rename to modules/compiler-core/src/internals/TimestampFormat.scala index cb60c0f..38d5145 100644 --- a/modules/openapi/src/internals/TimestampFormat.scala +++ b/modules/compiler-core/src/internals/TimestampFormat.scala @@ -13,10 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals -sealed abstract class TimestampFormat extends Product with Serializable -object TimestampFormat { +private[compiler] sealed abstract class TimestampFormat + extends Product + with Serializable + +private[compiler] object TimestampFormat { case object DateTime extends TimestampFormat // e.g. 2017-07-21T17:32:28Z case object SimpleDate extends TimestampFormat // e.g. 2017-07-21 } diff --git a/modules/openapi/src/internals/UnionKind.scala b/modules/compiler-core/src/internals/UnionKind.scala similarity index 87% rename from modules/openapi/src/internals/UnionKind.scala rename to modules/compiler-core/src/internals/UnionKind.scala index 6c68dcf..c09404e 100644 --- a/modules/openapi/src/internals/UnionKind.scala +++ b/modules/compiler-core/src/internals/UnionKind.scala @@ -13,9 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi.internals -sealed trait UnionKind -object UnionKind { +package smithytranslate.compiler.internals + +private[compiler] sealed trait UnionKind + +private[compiler] object UnionKind { case object Untagged extends UnionKind case object Tagged extends UnionKind case class Discriminated(field: String) extends UnionKind diff --git a/modules/openapi/src/internals/package.scala b/modules/compiler-core/src/internals/package.scala similarity index 65% rename from modules/openapi/src/internals/package.scala rename to modules/compiler-core/src/internals/package.scala index 4a873b0..80e21a7 100644 --- a/modules/openapi/src/internals/package.scala +++ b/modules/compiler-core/src/internals/package.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler import cats.mtl.Tell import cats.data.Chain @@ -21,10 +21,11 @@ import cats.data.NonEmptyChain package object internals { - type RefOr[A] = Either[String, A] - type Path = NonEmptyChain[String] - type TellShape[F[_]] = Tell[F, Chain[Either[Suppression, Definition]]] - type TellError[F[_]] = Tell[F, Chain[ModelError]] - type SecuritySchemes = Map[String, SecurityScheme] + private[compiler] type RefOr[A] = Either[String, A] + private[compiler] type Path = NonEmptyChain[String] + private[compiler] type TellShape[F[_]] = + Tell[F, Chain[Either[Suppression, Definition]]] + private[compiler] type TellError[F[_]] = Tell[F, Chain[ToSmithyError]] + private[compiler] type SecuritySchemes = Map[String, SecurityScheme] } diff --git a/modules/openapi/src/internals/postprocess/AllOfTransformer.scala b/modules/compiler-core/src/internals/postprocess/AllOfTransformer.scala similarity index 98% rename from modules/openapi/src/internals/postprocess/AllOfTransformer.scala rename to modules/compiler-core/src/internals/postprocess/AllOfTransformer.scala index a1951f3..09d841f 100644 --- a/modules/openapi/src/internals/postprocess/AllOfTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/AllOfTransformer.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess import cats.syntax.all._ @@ -22,7 +22,7 @@ import cats.kernel.Eq import org.typelevel.ci._ import scala.collection.mutable -object AllOfTransformer extends IModelPostProcessor { +private[compiler] object AllOfTransformer extends IModelPostProcessor { private val DocumentPrimitive = DefId(Namespace(List("smithy", "api")), Name.stdLib("Document")) diff --git a/modules/openapi/src/internals/postprocess/ContentTypeShiftTransformer.scala b/modules/compiler-core/src/internals/postprocess/ContentTypeShiftTransformer.scala similarity index 92% rename from modules/openapi/src/internals/postprocess/ContentTypeShiftTransformer.scala rename to modules/compiler-core/src/internals/postprocess/ContentTypeShiftTransformer.scala index d69b7f6..0c57d1f 100644 --- a/modules/openapi/src/internals/postprocess/ContentTypeShiftTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/ContentTypeShiftTransformer.scala @@ -13,14 +13,15 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess // If a shape is annotated with ContentTypeLabel, annotate all the members that point to it // with ContentTypeLabel and remove the ContentTypeLabel annotation on the shape // // This is useful for schemas coming from components/apiRequests -object ContentTypeShiftTransformer extends IModelPostProcessor { +private[compiler] object ContentTypeShiftTransformer + extends IModelPostProcessor { def apply(model: IModel): IModel = { val contentTypes = model.definitions.flatMap { d => diff --git a/modules/openapi/src/internals/postprocess/DiscriminatedTransformer.scala b/modules/compiler-core/src/internals/postprocess/DiscriminatedTransformer.scala similarity index 94% rename from modules/openapi/src/internals/postprocess/DiscriminatedTransformer.scala rename to modules/compiler-core/src/internals/postprocess/DiscriminatedTransformer.scala index 910f819..8a19f01 100644 --- a/modules/openapi/src/internals/postprocess/DiscriminatedTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/DiscriminatedTransformer.scala @@ -13,12 +13,12 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess import UnionKind._ -object DiscriminatedTransformer extends IModelPostProcessor { +private[compiler] object DiscriminatedTransformer extends IModelPostProcessor { private case class UnionInfo( discriminator: String, diff --git a/modules/openapi/src/internals/postprocess/DropRequiredWhenDefaultValue.scala b/modules/compiler-core/src/internals/postprocess/DropRequiredWhenDefaultValue.scala similarity index 91% rename from modules/openapi/src/internals/postprocess/DropRequiredWhenDefaultValue.scala rename to modules/compiler-core/src/internals/postprocess/DropRequiredWhenDefaultValue.scala index e009b17..019d5bb 100644 --- a/modules/openapi/src/internals/postprocess/DropRequiredWhenDefaultValue.scala +++ b/modules/compiler-core/src/internals/postprocess/DropRequiredWhenDefaultValue.scala @@ -13,12 +13,14 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess // If a member is marked as `required` but also has a `defaultValue`, we can // get rid of the `required` annotation. -object DropRequiredWhenDefaultValue extends IModelPostProcessor { +private[compiler] object DropRequiredWhenDefaultValue + extends IModelPostProcessor { + def apply(model: IModel): IModel = { val amendedDefs = model.definitions .map { diff --git a/modules/openapi/src/internals/postprocess/ExtensionsTransformer.scala b/modules/compiler-core/src/internals/postprocess/ExtensionsTransformer.scala similarity index 92% rename from modules/openapi/src/internals/postprocess/ExtensionsTransformer.scala rename to modules/compiler-core/src/internals/postprocess/ExtensionsTransformer.scala index 2e09606..93dcd75 100644 --- a/modules/openapi/src/internals/postprocess/ExtensionsTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/ExtensionsTransformer.scala @@ -13,14 +13,14 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess import software.amazon.smithy.model.node.Node import cats.syntax.all._ import cats.kernel.Semigroup -object ExtensionsMerger extends IModelPostProcessor { +private[compiler] object ExtensionsMerger extends IModelPostProcessor { def apply(model: IModel): IModel = IModel(model.definitions.map(process), model.suppressions) diff --git a/modules/openapi/src/internals/postprocess/ExternalMemberRefTransformer.scala b/modules/compiler-core/src/internals/postprocess/ExternalMemberRefTransformer.scala similarity index 96% rename from modules/openapi/src/internals/postprocess/ExternalMemberRefTransformer.scala rename to modules/compiler-core/src/internals/postprocess/ExternalMemberRefTransformer.scala index 608eb40..417446b 100644 --- a/modules/openapi/src/internals/postprocess/ExternalMemberRefTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/ExternalMemberRefTransformer.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess import org.typelevel.ci._ @@ -32,7 +32,9 @@ import scala.annotation.tailrec * no issue. The reason for this is that Structures get rendered as separate * types already that can be referenced from another file. */ -object ExternalMemberRefTransformer extends IModelPostProcessor { +private[compiler] object ExternalMemberRefTransformer + extends IModelPostProcessor { + private abstract class MatchesOne(val segment: Segment) { def unapply(segments: List[Segment]): Option[List[Segment]] = segments match { diff --git a/modules/openapi/src/internals/postprocess/FixMissingTargetsTransformer.scala b/modules/compiler-core/src/internals/postprocess/FixMissingTargetsTransformer.scala similarity index 94% rename from modules/openapi/src/internals/postprocess/FixMissingTargetsTransformer.scala rename to modules/compiler-core/src/internals/postprocess/FixMissingTargetsTransformer.scala index 3f123ce..2f4cc1d 100644 --- a/modules/openapi/src/internals/postprocess/FixMissingTargetsTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/FixMissingTargetsTransformer.scala @@ -13,10 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess -object FixMissingTargetsTransformer extends IModelPostProcessor { +private[compiler] object FixMissingTargetsTransformer + extends IModelPostProcessor { def apply(model: IModel): IModel = { val allDefIds = model.definitions.map(_.id).toSet diff --git a/modules/openapi/src/internals/postprocess/NewtypeTransformer.scala b/modules/compiler-core/src/internals/postprocess/NewtypeTransformer.scala similarity index 95% rename from modules/openapi/src/internals/postprocess/NewtypeTransformer.scala rename to modules/compiler-core/src/internals/postprocess/NewtypeTransformer.scala index 1ee204d..817874d 100644 --- a/modules/openapi/src/internals/postprocess/NewtypeTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/NewtypeTransformer.scala @@ -13,15 +13,16 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess -import smithytranslate.openapi.internals.Hint + +import smithytranslate.compiler.internals.Hint import scala.annotation.tailrec /** Removes "NewType" definitions by dereferencing them all in structures/union * definitions (could probably be do with more shapes) */ -object NewtypeTransformer extends IModelPostProcessor { +private[compiler] object NewtypeTransformer extends IModelPostProcessor { private def shouldRemainNewtype(hints: List[Hint]): Boolean = hints.exists { case _: Hint.Length => true case _: Hint.Range => true diff --git a/modules/openapi/src/internals/postprocess/ReorientDefaultValueTransformer.scala b/modules/compiler-core/src/internals/postprocess/ReorientDefaultValueTransformer.scala similarity index 92% rename from modules/openapi/src/internals/postprocess/ReorientDefaultValueTransformer.scala rename to modules/compiler-core/src/internals/postprocess/ReorientDefaultValueTransformer.scala index 3563904..7230fa6 100644 --- a/modules/openapi/src/internals/postprocess/ReorientDefaultValueTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/ReorientDefaultValueTransformer.scala @@ -13,12 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess // If a non-member shape is annotated with DefaultValue, annotate all the members that point to it // with DefaultValue and remove the DefaultValue annotation on the shape -object ReorientDefaultValueTransformer extends IModelPostProcessor { +private[compiler] object ReorientDefaultValueTransformer + extends IModelPostProcessor { def apply(model: IModel): IModel = { val defaultValues = model.definitions.flatMap { d => diff --git a/modules/openapi/src/internals/postprocess/RequirementShiftTransformer.scala b/modules/compiler-core/src/internals/postprocess/RequirementShiftTransformer.scala similarity index 92% rename from modules/openapi/src/internals/postprocess/RequirementShiftTransformer.scala rename to modules/compiler-core/src/internals/postprocess/RequirementShiftTransformer.scala index ed47488..bdb2faf 100644 --- a/modules/openapi/src/internals/postprocess/RequirementShiftTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/RequirementShiftTransformer.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess // If a shape is annotated as Required, annotate all the members that point to it as required @@ -21,7 +21,8 @@ package postprocess // // This is useful for schemas coming from components/apiRequests which contain a "required" // property -object RequirementShiftTransformer extends IModelPostProcessor { +private[compiler] object RequirementShiftTransformer + extends IModelPostProcessor { def apply(model: IModel): IModel = { val isRequired = model.definitions.collect { diff --git a/modules/openapi/src/internals/postprocess/SimplifyNameTransformer.scala b/modules/compiler-core/src/internals/postprocess/SimplifyNameTransformer.scala similarity index 97% rename from modules/openapi/src/internals/postprocess/SimplifyNameTransformer.scala rename to modules/compiler-core/src/internals/postprocess/SimplifyNameTransformer.scala index 24319c3..8b97987 100644 --- a/modules/openapi/src/internals/postprocess/SimplifyNameTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/SimplifyNameTransformer.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess import cats.data.NonEmptyChain @@ -21,7 +21,7 @@ import cats.syntax.all._ import org.typelevel.ci._ import scala.collection.compat._ -object SimplifyNameTransformer extends IModelPostProcessor { +private[compiler] object SimplifyNameTransformer extends IModelPostProcessor { private type OriginalName = Name private type UpdatedName = Name diff --git a/modules/openapi/src/internals/postprocess/TaggedUnionTransformer.scala b/modules/compiler-core/src/internals/postprocess/TaggedUnionTransformer.scala similarity index 96% rename from modules/openapi/src/internals/postprocess/TaggedUnionTransformer.scala rename to modules/compiler-core/src/internals/postprocess/TaggedUnionTransformer.scala index cb66baa..5dbe429 100644 --- a/modules/openapi/src/internals/postprocess/TaggedUnionTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/TaggedUnionTransformer.scala @@ -13,12 +13,12 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals package postprocess import scala.collection.compat._ -object TaggedUnionTransformer extends IModelPostProcessor { +private[compiler] object TaggedUnionTransformer extends IModelPostProcessor { private case class TaggedUnionInfo( taggedFields: Vector[Field], diff --git a/modules/openapi/src/internals/recursion.scala b/modules/compiler-core/src/internals/recursion.scala similarity index 97% rename from modules/openapi/src/internals/recursion.scala rename to modules/compiler-core/src/internals/recursion.scala index c620011..d4b3250 100644 --- a/modules/openapi/src/internals/recursion.scala +++ b/modules/compiler-core/src/internals/recursion.scala @@ -13,7 +13,8 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals + import cats._ import cats.syntax.all._ @@ -23,7 +24,7 @@ import cats.syntax.all._ * These functions allow to decouple the recursive aspect of model construction * and validation, from the domain-specific concerns. */ -object recursion { +private[compiler] object recursion { type Fold[Pattern[_], A] = Pattern[A] => A type FoldF[F[_], Pattern[_], A] = Pattern[A] => F[A] diff --git a/modules/json-schema/src/JsonSchemaCompiler.scala b/modules/json-schema/src/JsonSchemaCompiler.scala index b75e3ba..24e3248 100644 --- a/modules/json-schema/src/JsonSchemaCompiler.scala +++ b/modules/json-schema/src/JsonSchemaCompiler.scala @@ -13,25 +13,54 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema -import software.amazon.smithy.model.{Model => SmithyModel} import cats.syntax.all._ import cats.data.NonEmptyChain -import smithytranslate.json_schema.internals.JsonSchemaToIModel -import smithytranslate.openapi.internals.IModelPostProcessor -import smithytranslate.openapi.internals.IModelToSmithy +import smithytranslate.compiler.internals.json_schema.JsonSchemaToIModel import cats.data.NonEmptyList -import software.amazon.smithy.build.TransformContext import org.everit.json.schema.Schema import org.json.JSONObject -import smithytranslate.openapi.OpenApiCompiler._ import io.circe.Json -import smithytranslate.json_schema.internals.LoadSchema +import smithytranslate.compiler.internals.json_schema._ +import smithytranslate.compiler.internals.IModel +import smithytranslate.compiler.ToSmithyCompilerOptions +import smithytranslate.compiler.AbstractToSmithyCompiler +import JsonSchemaCompilerInput._ +import cats.data.Chain +import smithytranslate.compiler.ToSmithyError +import smithytranslate.compiler.FileContents /** Converts json schema to a smithy model. */ -object JsonSchemaCompiler { +object JsonSchemaCompiler + extends AbstractToSmithyCompiler[JsonSchemaCompilerInput] { + + protected def convertToInternalModel( + opts: ToSmithyCompilerOptions, + input: JsonSchemaCompilerInput + ): (Chain[ToSmithyError], IModel) = { + val prepared = input match { + case UnparsedSpecs(specs) => + val parser: String => Schema = + schemaString => LoadSchema(new JSONObject(schemaString)) + specs.map { case FileContents(path, content) => + val ns = NonEmptyChain.fromNonEmptyList(removeFileExtension(path)) + val schema = parser(content) + val json = io.circe.jawn.parse(content) match { + case Left(error) => throw error + case Right(value) => value + } + (ns, schema, json) + } + case ParsedSpec(path, rawJson, schema) => + val ns = NonEmptyChain.fromNonEmptyList(removeFileExtension(path)) + List((ns, schema, rawJson)) + } + prepared.foldMap { case (ns, schema, rawJson) => + JsonSchemaToIModel.compile(ns, schema, rawJson) + } + } type Input = (NonEmptyChain[String], Schema, Json) @@ -46,52 +75,4 @@ object JsonSchemaCompiler { ) } - def parseAndCompile( - opts: Options, - stringInputs: (NonEmptyList[String], String)* - ): Result[SmithyModel] = { - val parseSchema: String => Schema = schemaString => - LoadSchema(new JSONObject(schemaString)) - val inputs = - stringInputs.map { case (path, content) => - val ns = NonEmptyChain.fromNonEmptyList(removeFileExtension(path)) - val schema = parseSchema(content) - val json = io.circe.jawn.parse(content) match { - case Left(error) => throw error - case Right(value) => value - } - (ns, schema, json) - } - compile(opts, inputs: _*) - } - - def compile( - opts: Options, - inputs: Input* - ): Result[SmithyModel] = { - val (errors0, smithy0) = inputs.toList - .foldMap { case (ns, s, raw) => JsonSchemaToIModel.compile(ns, s, raw) } - .map(IModelPostProcessor(opts.useVerboseNames)) - .map(new IModelToSmithy(opts.useEnumTraitSyntax)) - val errors = errors0.toList - - scala.util - .Try(validate(smithy0)) - .toEither - .leftMap(opts.debugModelValidationError) - .map(transform(opts)) - .fold( - err => Failure(err, errors), - model => Success(errors.toList, model) - ) - } - - private def validate(model: SmithyModel): SmithyModel = - SmithyModel.assembler().discoverModels().addModel(model).assemble().unwrap() - - private def transform(opts: Options)(model: SmithyModel): SmithyModel = - opts.transformers.foldLeft(model)((m, t) => - t.transform(TransformContext.builder().model(m).build()) - ) - } diff --git a/modules/json-schema/src/JsonSchemaCompilerInput.scala b/modules/json-schema/src/JsonSchemaCompilerInput.scala new file mode 100644 index 0000000..a80d07c --- /dev/null +++ b/modules/json-schema/src/JsonSchemaCompilerInput.scala @@ -0,0 +1,35 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler.json_schema + +import cats.data.NonEmptyList +import org.everit.json.schema.Schema +import io.circe.Json +import smithytranslate.compiler.FileContents + +sealed trait JsonSchemaCompilerInput + +object JsonSchemaCompilerInput { + final case class UnparsedSpecs(specs: List[FileContents]) + extends JsonSchemaCompilerInput + + final case class ParsedSpec( + path: NonEmptyList[String], + rawJson: Json, + schema: Schema + ) extends JsonSchemaCompilerInput + +} diff --git a/modules/json-schema/src/internals/EmptyValidator.scala b/modules/json-schema/src/internals/EmptyValidator.scala index ed47b61..85520b4 100644 --- a/modules/json-schema/src/internals/EmptyValidator.scala +++ b/modules/json-schema/src/internals/EmptyValidator.scala @@ -13,12 +13,13 @@ * limitations under the License. */ -package smithytranslate.json_schema.internals +package smithytranslate.compiler.internals.json_schema import org.everit.json.schema.FormatValidator import java.util.Optional -final class EmptyValidator(name: String) extends FormatValidator { +private[json_schema] final class EmptyValidator(name: String) + extends FormatValidator { override def validate(subject: String): Optional[String] = Optional.empty() override def formatName(): String = name } diff --git a/modules/json-schema/src/internals/Extractors.scala b/modules/json-schema/src/internals/Extractors.scala index c892d88..baeb45e 100644 --- a/modules/json-schema/src/internals/Extractors.scala +++ b/modules/json-schema/src/internals/Extractors.scala @@ -13,23 +13,21 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler package internals +package json_schema -import smithytranslate.openapi.internals.Primitive._ +import Primitive._ import org.everit.json.schema.Schema import org.everit.json.schema._ -import smithytranslate.openapi.internals.Primitive import cats.data.NonEmptyChain -import smithytranslate.openapi.internals.DefId -import smithytranslate.openapi.ModelError -import smithytranslate.openapi.internals.Hint -import smithytranslate.openapi.internals.GetExtensions +import smithytranslate.compiler.ToSmithyError +import smithytranslate.compiler.internals.GetExtensions import scala.jdk.CollectionConverters._ import cats.syntax.all._ import scala.collection.compat._ -object Extractors { +private[json_schema] object Extractors { type Path = NonEmptyChain[String] @@ -252,8 +250,8 @@ object Extractors { * The most complicated thing */ abstract class JsonSchemaCaseRefBuilder(id: Option[String], ns: Path) - extends smithytranslate.openapi.internals.CaseRefBuilder(ns) { - def unapply(sch: Schema): Option[Either[ModelError, DefId]] = sch match { + extends smithytranslate.compiler.internals.RefParser(ns) { + def unapply(sch: Schema): Option[Either[ToSmithyError, DefId]] = sch match { case ref: ReferenceSchema => // For some reason the reference seems to get prefixed by the id every now and then val refValue = ref.getReferenceValue() diff --git a/modules/json-schema/src/internals/JsonSchemaToIModel.scala b/modules/json-schema/src/internals/JsonSchemaToIModel.scala index 0bc3cf3..0361703 100644 --- a/modules/json-schema/src/internals/JsonSchemaToIModel.scala +++ b/modules/json-schema/src/internals/JsonSchemaToIModel.scala @@ -13,33 +13,32 @@ * limitations under the License. */ -package smithytranslate.json_schema.internals +package smithytranslate.compiler +package internals +package json_schema import cats.Parallel import scala.jdk.CollectionConverters._ import cats.data._ import cats.syntax.all._ -import smithytranslate.openapi.ModelError -import smithytranslate.openapi.ModelError._ import cats.Monad import org.typelevel.ci._ import org.everit.json.schema.{Schema => ESchema} -import smithytranslate.openapi.internals.{Local => _, _} -import smithytranslate.openapi.internals.Suppression +import smithytranslate.compiler.internals.Suppression import Extractors._ import org.json.JSONObject import io.circe.Json import io.circe.ACursor import io.circe.JsonObject -object JsonSchemaToIModel { +private[compiler] object JsonSchemaToIModel { def compile( namespace: Path, jsonSchema: ESchema, rawJson: Json - ): (Chain[ModelError], IModel) = { - type ErrorLayer[A] = Writer[Chain[ModelError], A] + ): (Chain[ToSmithyError], IModel) = { + type ErrorLayer[A] = Writer[Chain[ToSmithyError], A] type WriterLayer[A] = WriterT[ErrorLayer, Chain[Either[Suppression, Definition]], A] val (errors, (data, _)) = @@ -124,7 +123,7 @@ private class JsonSchemaToIModel[F[_]: Parallel: TellShape: TellError]( val recordAll = refoldSchemas - private def fold = new OpenApiToIModel.OpenApiFolder[F](namespace).fold _ + private def fold = new PatternFolder[F](namespace).fold _ private def refoldOne(start: Local): F[DefId] = { // Refolding each top value under openapi's "component/schema" @@ -282,7 +281,7 @@ private class JsonSchemaToIModel[F[_]: Parallel: TellShape: TellError]( F.pure(OpenApiNull(local.context)) case s => - val error = Restriction(s"Schema not supported:\n$s") + val error = ToSmithyError.Restriction(s"Schema not supported:\n$s") F.pure(OpenApiShortStop(local.context, error)) } } diff --git a/modules/json-schema/src/internals/LoadSchema.scala b/modules/json-schema/src/internals/LoadSchema.scala index a10b3cb..0788ac7 100644 --- a/modules/json-schema/src/internals/LoadSchema.scala +++ b/modules/json-schema/src/internals/LoadSchema.scala @@ -13,13 +13,15 @@ * limitations under the License. */ -package smithytranslate.json_schema.internals +package smithytranslate.compiler +package internals +package json_schema import org.everit.json.schema.Schema import org.json.JSONObject import org.everit.json.schema.loader.SchemaLoader -object LoadSchema extends (JSONObject => Schema) { +private[compiler] object LoadSchema extends (JSONObject => Schema) { def apply(sch: JSONObject): Schema = SchemaLoader .builder() diff --git a/modules/json-schema/src/internals/Local.scala b/modules/json-schema/src/internals/Local.scala index 5358d36..780e111 100644 --- a/modules/json-schema/src/internals/Local.scala +++ b/modules/json-schema/src/internals/Local.scala @@ -13,40 +13,42 @@ * limitations under the License. */ -package smithytranslate.json_schema.internals +package smithytranslate.compiler +package internals +package json_schema import cats.data.NonEmptyChain -import smithytranslate.openapi.internals.{Local => _, _} +import smithytranslate.compiler.internals._ import org.everit.json.schema.Schema import io.circe.Json -case class Local( +private[json_schema] case class Local( context: Context, schema: Schema, topLevelJson: Json ) { - def mapPath(f: Name => Name): Local = + final def mapPath(f: Name => Name): Local = copy(context = context.copy(path = f(context.path))) - def addHints(newHints: List[Hint]): Local = + final def addHints(newHints: List[Hint]): Local = copy(context = context.copy(hints = newHints ++ context.hints)) - def addHints(newHints: Hint*): Local = + final def addHints(newHints: Hint*): Local = this.addHints(newHints.toList) - def filterHints(f: Hint => Boolean): Local = + final def filterHints(f: Hint => Boolean): Local = copy(context = context.copy(hints = context.hints.filter(f))) - def down(segment: Segment, subSchema: Schema): Local = + final def down(segment: Segment, subSchema: Schema): Local = new Local(context.append(segment), subSchema, topLevelJson) - def down(name: Name, subSchema: Schema): Local = + final def down(name: Name, subSchema: Schema): Local = new Local(context.append(name), subSchema, topLevelJson) } -object Local { +private[json_schema] object Local { def apply(path: Name, schema: Schema, topLevelJson: Json): Local = Local(Context(path, Nil, Nil), schema, topLevelJson) diff --git a/modules/json-schema/tests/src/AllOfSpec.scala b/modules/json-schema/tests/src/AllOfSpec.scala index 38aeb49..6610bd8 100644 --- a/modules/json-schema/tests/src/AllOfSpec.scala +++ b/modules/json-schema/tests/src/AllOfSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class AllOfSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/ArraySpec.scala b/modules/json-schema/tests/src/ArraySpec.scala index 8ea6c41..470cee6 100644 --- a/modules/json-schema/tests/src/ArraySpec.scala +++ b/modules/json-schema/tests/src/ArraySpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class ArraySpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/ConstraintSpec.scala b/modules/json-schema/tests/src/ConstraintSpec.scala index 2f35979..c60efbb 100644 --- a/modules/json-schema/tests/src/ConstraintSpec.scala +++ b/modules/json-schema/tests/src/ConstraintSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class ConstraintSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/DefaultValueSpec.scala b/modules/json-schema/tests/src/DefaultValueSpec.scala index cd67f76..17f3020 100644 --- a/modules/json-schema/tests/src/DefaultValueSpec.scala +++ b/modules/json-schema/tests/src/DefaultValueSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class DefaultValueSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/EnumSpec.scala b/modules/json-schema/tests/src/EnumSpec.scala index 50df97e..0b5dadf 100644 --- a/modules/json-schema/tests/src/EnumSpec.scala +++ b/modules/json-schema/tests/src/EnumSpec.scala @@ -13,9 +13,9 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema -import smithytranslate.openapi.OpenApiCompiler.SmithyVersion +import smithytranslate.compiler.SmithyVersion final class EnumSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/FileBasedSpec.scala b/modules/json-schema/tests/src/FileBasedSpec.scala index be09983..fe130ff 100644 --- a/modules/json-schema/tests/src/FileBasedSpec.scala +++ b/modules/json-schema/tests/src/FileBasedSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema import cats.data.NonEmptyList diff --git a/modules/json-schema/tests/src/MapSpec.scala b/modules/json-schema/tests/src/MapSpec.scala index 5d7d9b9..9e4bde7 100644 --- a/modules/json-schema/tests/src/MapSpec.scala +++ b/modules/json-schema/tests/src/MapSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class MapSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/NullSpec.scala b/modules/json-schema/tests/src/NullSpec.scala index b2dd2a2..b21cf11 100644 --- a/modules/json-schema/tests/src/NullSpec.scala +++ b/modules/json-schema/tests/src/NullSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class NullSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/PrimitiveSpec.scala b/modules/json-schema/tests/src/PrimitiveSpec.scala index bde6955..e98be55 100644 --- a/modules/json-schema/tests/src/PrimitiveSpec.scala +++ b/modules/json-schema/tests/src/PrimitiveSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class PrimitiveSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/RefSpec.scala b/modules/json-schema/tests/src/RefSpec.scala index ddb3a8b..8516558 100644 --- a/modules/json-schema/tests/src/RefSpec.scala +++ b/modules/json-schema/tests/src/RefSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class RefSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/SetSpec.scala b/modules/json-schema/tests/src/SetSpec.scala index e311240..850cbb4 100644 --- a/modules/json-schema/tests/src/SetSpec.scala +++ b/modules/json-schema/tests/src/SetSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class SetSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/SpecialFormatSpec.scala b/modules/json-schema/tests/src/SpecialFormatSpec.scala index 1f4f2be..09596e7 100644 --- a/modules/json-schema/tests/src/SpecialFormatSpec.scala +++ b/modules/json-schema/tests/src/SpecialFormatSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class SpecialFormatSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/StructureSpec.scala b/modules/json-schema/tests/src/StructureSpec.scala index 3791ea8..efc66a9 100644 --- a/modules/json-schema/tests/src/StructureSpec.scala +++ b/modules/json-schema/tests/src/StructureSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class StructureSpec extends munit.FunSuite { diff --git a/modules/json-schema/tests/src/TestUtils.scala b/modules/json-schema/tests/src/TestUtils.scala index ed21cc8..fb9d1b2 100644 --- a/modules/json-schema/tests/src/TestUtils.scala +++ b/modules/json-schema/tests/src/TestUtils.scala @@ -13,20 +13,23 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer -import scala.jdk.CollectionConverters._ -import munit.Assertions -import software.amazon.smithy.build.transforms.FilterSuppressions -import software.amazon.smithy.build.TransformContext +import cats.data.NonEmptyList import cats.syntax.all._ +import munit.Assertions import munit.Location -import cats.data.NonEmptyList +import smithytranslate.compiler.FileContents +import smithytranslate.compiler.SmithyVersion +import smithytranslate.compiler.ToSmithyCompilerOptions +import smithytranslate.compiler.ToSmithyResult +import software.amazon.smithy.build.TransformContext +import software.amazon.smithy.build.transforms.FilterSuppressions +import software.amazon.smithy.model.Model import software.amazon.smithy.model.node._ -import smithytranslate.openapi.OpenApiCompiler -import smithytranslate.openapi.OpenApiCompiler.SmithyVersion +import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer + +import scala.jdk.CollectionConverters._ object TestUtils { @@ -39,7 +42,7 @@ object TestUtils { ) final case class ConversionResult( - result: OpenApiCompiler.Result[ModelWrapper], + result: ToSmithyResult[ModelWrapper], expected: ModelWrapper ) @@ -49,8 +52,8 @@ object TestUtils { ): ConversionResult = { val inputs = input0 +: remaining val result = - JsonSchemaCompiler.parseAndCompile( - OpenApiCompiler.Options( + JsonSchemaCompiler.compile( + ToSmithyCompilerOptions( useVerboseNames = false, validateInput = false, validateOutput = false, @@ -58,7 +61,11 @@ object TestUtils { input0.smithyVersion == SmithyVersion.One, debug = true ), - inputs.map(i => i.filePath -> i.jsonSpec): _* + JsonSchemaCompilerInput.UnparsedSpecs( + inputs + .map(i => FileContents(i.filePath, i.jsonSpec)) + .toList + ) ) val resultW = result.map(ModelWrapper(_)) @@ -92,10 +99,10 @@ object TestUtils { remaining: _* ) res match { - case OpenApiCompiler.Failure(err, errors) => + case ToSmithyResult.Failure(err, errors) => errors.foreach(println) Assertions.fail("Validating model failed: ", err) - case OpenApiCompiler.Success(_, output) => + case ToSmithyResult.Success(_, output) => Assertions.assertEquals(output, expected) } } diff --git a/modules/json-schema/tests/src/UnionSpec.scala b/modules/json-schema/tests/src/UnionSpec.scala index 65b71fb..34b8327 100644 --- a/modules/json-schema/tests/src/UnionSpec.scala +++ b/modules/json-schema/tests/src/UnionSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.json_schema +package smithytranslate.compiler.json_schema final class UnionSpec extends munit.FunSuite { diff --git a/modules/openapi/src/OpenApiCompiler.scala b/modules/openapi/src/OpenApiCompiler.scala index b2b5094..825fb34 100644 --- a/modules/openapi/src/OpenApiCompiler.scala +++ b/modules/openapi/src/OpenApiCompiler.scala @@ -13,121 +13,37 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi -import scala.jdk.OptionConverters._ -import io.swagger.v3.oas.models.OpenAPI -import software.amazon.smithy.model.{Model => SmithyModel} -import cats.syntax.all._ +import cats.data.Chain import cats.data.NonEmptyChain -import smithytranslate.openapi.internals.OpenApiToIModel -import smithytranslate.openapi.internals.IModelPostProcessor -import smithytranslate.openapi.internals.IModelToSmithy -import io.swagger.parser.OpenAPIParser import cats.data.NonEmptyList +import cats.syntax.all._ +import io.swagger.parser.OpenAPIParser +import smithytranslate.compiler._ +import smithytranslate.compiler.internals._ +import smithytranslate.compiler.internals.openapi.OpenApiToIModel + import scala.jdk.CollectionConverters._ -import software.amazon.smithy.build.ProjectionTransformer -import software.amazon.smithy.build.TransformContext -import cats.Functor -import smithytranslate.openapi.OpenApiCompiler.SmithyVersion.One -import smithytranslate.openapi.OpenApiCompiler.SmithyVersion.Two -import software.amazon.smithy.model.validation.ValidatedResultException -import software.amazon.smithy.model.validation.Severity -import java.util.stream.Collectors -import software.amazon.smithy.model.validation.ValidatedResult + +import OpenApiCompilerInput._ /** Converts openapi to a smithy model. */ -object OpenApiCompiler { - - // Either the smithy validation fails, in which case we get a left with - // the list of errors. Or smithy validation works and we get a pair of - // errors (that still pass smithy validation) and a smithy model - sealed trait Result[+A] - case class Failure[A](cause: Throwable, errors: List[ModelError]) - extends Result[A] - case class Success[A](error: List[ModelError], value: A) extends Result[A] - - object Result { - implicit val functor: Functor[Result] = new Functor[Result]() { - - override def map[A, B](fa: Result[A])(f: A => B): Result[B] = fa match { - case Failure(cause, errors) => Failure(cause, errors) - case Success(error, value) => Success(error, f(value)) - } - - } - } - - sealed abstract class SmithyVersion extends Product with Serializable { - override def toString(): String = this match { - case One => "1.0" - case Two => "2.0" - } - } - object SmithyVersion { - case object One extends SmithyVersion - case object Two extends SmithyVersion - - def fromString(string: String): Either[String, SmithyVersion] = { - val versionOne = Set("1", "1.0") - val versionTwo = Set("2", "2.0") - if (versionOne(string)) Right(SmithyVersion.One) - else if (versionTwo(string)) Right(SmithyVersion.Two) - else - Left( - s"expected one of ${versionOne ++ versionTwo}, but got '$string'" - ) - } - } - - final case class Options( - useVerboseNames: Boolean, - validateInput: Boolean, - validateOutput: Boolean, - transformers: List[ProjectionTransformer], - useEnumTraitSyntax: Boolean, - debug: Boolean - ) { - val debugModelValidationError: Throwable => Throwable = { - case ex: ValidatedResultException if !debug => - new ValidatedResultException( - ex.getValidationEvents() - .stream() - .filter(err => - err.getSeverity == Severity.ERROR || err.getSeverity == Severity.DANGER - ) - .collect(Collectors.toList()) - ) - case ex: Throwable => ex - } - } - - type Input = (NonEmptyList[String], Either[List[String], OpenAPI]) - - private def removeFileExtension( - path: NonEmptyList[String] - ): NonEmptyList[String] = { - val lastSplit = path.last.split('.') - val newLast = - if (lastSplit.size > 1) lastSplit.dropRight(1) else lastSplit - NonEmptyList.fromListUnsafe( - path.toList.dropRight(1) :+ newLast.mkString(".") - ) - } - - def parseAndCompile( - opts: Options, - stringInputs: (NonEmptyList[String], String)* - ): Result[SmithyModel] = { - val parser = new OpenAPIParser() - val openapiInputs = - stringInputs.map( - _.bimap( - removeFileExtension, - in => { - val result = parser.readContents(in, null, null) - +object OpenApiCompiler extends AbstractToSmithyCompiler[OpenApiCompilerInput] { + + protected def convertToInternalModel( + opts: ToSmithyCompilerOptions, + input: OpenApiCompilerInput + ): (Chain[ToSmithyError], IModel) = { + val prepared = input match { + case UnparsedSpecs(specs) => + val parser = new OpenAPIParser() + specs.map { case FileContents(path, content) => + val cleanedPath = removeFileExtension(path) + val result = parser.readContents(content, null, null) + + val parsed = if ( opts.validateInput && !result .getMessages() @@ -141,74 +57,26 @@ object OpenApiCompiler { Option(result.getOpenAPI()) .toRight(result.getMessages().asScala.toList) } - } - ) - ) - compile(opts, openapiInputs: _*) - } - - def compile( - opts: Options, - inputs: Input* - ): Result[SmithyModel] = { - val (errors0, smithy0) = inputs.toList - .map(_.leftMap(NonEmptyChain.fromNonEmptyList)) - .foldMap { case (c, e) => OpenApiToIModel.compile(c, e) } - .map(IModelPostProcessor(opts.useVerboseNames)) - .map(new IModelToSmithy(opts.useEnumTraitSyntax)) - val translationErrors = errors0.toList - - val assembled: ValidatedResult[SmithyModel] = validate(smithy0) - val validationEvents = if (opts.debug) { - assembled.getValidationEvents() - } else { - val errorEvents = assembled.getValidationEvents(Severity.ERROR) - val dangerEvents = assembled.getValidationEvents(Severity.DANGER) - val criticalErrors = new java.util.ArrayList(errorEvents) - criticalErrors.addAll(dangerEvents) - criticalErrors - } - val problematicSeverities = Set(Severity.DANGER, Severity.ERROR) - val hasProblematicEvents = validationEvents - .iterator() - .asScala - .map(_.getSeverity()) - .exists(problematicSeverities) - - val allErrors = - if (hasProblematicEvents) { - val smithyValidationFailed = - ModelError.SmithyValidationFailed(validationEvents.asScala.toList) - smithyValidationFailed :: translationErrors - } else { - translationErrors - } - - val transformedModel = - assembled.getResult().toScala.map(transform(opts)) - - (transformedModel, NonEmptyList.fromList(allErrors)) match { - case (None, Some(nonEmptyErrors)) => - Failure(nonEmptyErrors.head, nonEmptyErrors.tail) - case (None, None) => - Failure(ModelError.SmithyValidationFailed(Nil), translationErrors) - case (Some(model), None) => - Success(translationErrors, model) - case (Some(model), Some(nonEmptyErrors)) => - if (opts.validateOutput) { - Failure(nonEmptyErrors.head, nonEmptyErrors.tail) - } else { - Success(allErrors, model) + (cleanedPath, parsed) } + case ParsedSpec(path, openapiModel) => + val cleanedPath = removeFileExtension(path) + List(cleanedPath -> Right(openapiModel)) + } + prepared.foldMap { case (path, parsed) => + OpenApiToIModel.compile(NonEmptyChain.fromNonEmptyList(path), parsed) } } - private def validate(model: SmithyModel): ValidatedResult[SmithyModel] = - SmithyModel.assembler().discoverModels().addModel(model).assemble() - - private def transform(opts: Options)(model: SmithyModel): SmithyModel = - opts.transformers.foldLeft(model)((m, t) => - t.transform(TransformContext.builder().model(m).build()) + private def removeFileExtension( + path: NonEmptyList[String] + ): NonEmptyList[String] = { + val lastSplit = path.last.split('.') + val newLast = + if (lastSplit.size > 1) lastSplit.dropRight(1) else lastSplit + NonEmptyList.fromListUnsafe( + path.toList.dropRight(1) :+ newLast.mkString(".") ) + } } diff --git a/modules/openapi/src/OpenApiCompilerInput.scala b/modules/openapi/src/OpenApiCompilerInput.scala new file mode 100644 index 0000000..7fa6f00 --- /dev/null +++ b/modules/openapi/src/OpenApiCompilerInput.scala @@ -0,0 +1,31 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithytranslate.compiler.openapi + +import cats.data.NonEmptyList +import io.swagger.v3.oas.models.OpenAPI +import smithytranslate.compiler.FileContents + +sealed trait OpenApiCompilerInput + +object OpenApiCompilerInput { + case class UnparsedSpecs(files: List[FileContents]) + extends OpenApiCompilerInput + + case class ParsedSpec(path: NonEmptyList[String], openapiModel: OpenAPI) + extends OpenApiCompilerInput + +} diff --git a/modules/openapi/src/internals/ApiResponseToParams.scala b/modules/openapi/src/internals/ApiResponseToParams.scala index f7cc6c7..26db59d 100644 --- a/modules/openapi/src/internals/ApiResponseToParams.scala +++ b/modules/openapi/src/internals/ApiResponseToParams.scala @@ -13,15 +13,17 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.responses.ApiResponse -import smithytranslate.openapi.internals.GetExtensions.HasExtensions +import GetExtensions.HasExtensions import scala.jdk.CollectionConverters._ -object ApiResponseToParams +private[openapi] object ApiResponseToParams extends ((Name, ApiResponse) => RefOr[HttpMessageInfo]) { def apply( diff --git a/modules/openapi/src/internals/ContentToSchemaOpt.scala b/modules/openapi/src/internals/ContentToSchemaOpt.scala index 5eacd31..955be2b 100644 --- a/modules/openapi/src/internals/ContentToSchemaOpt.scala +++ b/modules/openapi/src/internals/ContentToSchemaOpt.scala @@ -13,14 +13,17 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi import io.swagger.v3.oas.models.media.Schema import scala.jdk.CollectionConverters._ import io.swagger.v3.oas.models.media.Content import scala.collection.compat._ -object ContentToSchemaOpt extends (Content => Map[String, Schema[_]]) { +private[openapi] object ContentToSchemaOpt + extends (Content => Map[String, Schema[_]]) { def apply(c: Content): Map[String, Schema[_]] = Option(c) diff --git a/modules/openapi/src/internals/ContentTypeDiscriminatedSchema.scala b/modules/openapi/src/internals/ContentTypeDiscriminatedSchema.scala index aba1006..d84560f 100644 --- a/modules/openapi/src/internals/ContentTypeDiscriminatedSchema.scala +++ b/modules/openapi/src/internals/ContentTypeDiscriminatedSchema.scala @@ -12,10 +12,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi import io.swagger.v3.oas.models.media.Schema -final case class ContentTypeDiscriminatedSchema(members: Map[String, Schema[_]]) - extends Schema[Any] +private[openapi] final case class ContentTypeDiscriminatedSchema( + members: Map[String, Schema[_]] +) extends Schema[Any] diff --git a/modules/openapi/src/internals/Extractors.scala b/modules/openapi/src/internals/Extractors.scala index 57b6841..6f4bd83 100644 --- a/modules/openapi/src/internals/Extractors.scala +++ b/modules/openapi/src/internals/Extractors.scala @@ -13,17 +13,17 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler package internals +package openapi +import smithytranslate.compiler._ +import smithytranslate.compiler.internals._ import io.swagger.v3.oas.models.media._ import scala.jdk.CollectionConverters._ -import cats.data.NonEmptyChain -import cats.syntax.all._ import Primitive._ -import org.typelevel.ci.CIString -object CaseEnum { +private[openapi] object CaseEnum { def unapply(sch: Schema[_]): Option[Vector[String]] = sch match { case s: StringSchema if s.getEnum() != null && !s.getEnum().isEmpty => Some(s.getEnum().asScala.toVector) @@ -31,7 +31,7 @@ object CaseEnum { } } -object CaseMap { +private[openapi] object CaseMap { def unapply(sch: Schema[_]): Option[Schema[_]] = { sch match { case m: MapSchema => @@ -43,7 +43,7 @@ object CaseMap { } } -object IsFreeForm { +private[openapi] object IsFreeForm { def unapply(sch: Schema[_]): Boolean = { val isObjectSchema = sch.isInstanceOf[ObjectSchema] val hasNoProperties = Option(sch.getProperties()).forall(_.isEmpty()) @@ -56,19 +56,19 @@ object IsFreeForm { } } -object Format { +private[openapi] object Format { def unapply(sch: Schema[_]): Option[String] = Option(sch.getFormat()) } -object NoFormat { +private[openapi] object NoFormat { def unapply(sch: Schema[_]): Boolean = Option(sch.getFormat()).isEmpty } -object & { +private[openapi] object & { def unapply[A](a: A): Some[(A, A)] = Some((a, a)) } -object CaseAllOf { +private[openapi] object CaseAllOf { def unapply(sch: Schema[_]): Option[Vector[Schema[_]]] = sch match { case composed: ComposedSchema => Option(composed.getAllOf()).map(_.asScala.toVector) @@ -76,7 +76,7 @@ object CaseAllOf { } } -object CaseOneOf { +private[openapi] object CaseOneOf { def unapply(sch: Schema[_]): Option[Vector[Schema[_]]] = sch match { case composed: ComposedSchema => Option(composed.getOneOf()).map(_.asScala.toVector) @@ -84,7 +84,7 @@ object CaseOneOf { } } -object CaseObject { +private[openapi] object CaseObject { def unapply(sch: Schema[_]): Option[Schema[_]] = sch match { case o: ObjectSchema => Some(o) case s: Schema[_] if Option(s.getProperties()).exists(_.asScala.nonEmpty) => @@ -93,14 +93,14 @@ object CaseObject { } } -object NonEmptySegments { +private[openapi] object NonEmptySegments { def unapply(s: String): Option[(List[String], String)] = { val segments = s.split("/").toList.filterNot(_.isEmpty()) segments.lastOption.map(last => segments.dropRight(1) -> last) } } -object CasePrimitive { +private[openapi] object CasePrimitive { def unapply(sch: Schema[_]): Option[Primitive] = sch match { // S: // type: string @@ -176,79 +176,8 @@ object CasePrimitive { /* * The most complicated thing */ -abstract class CaseRefBuilder(ns: Path) { - def unapply(sch: Schema[_]): Option[Either[ModelError, DefId]] = +private[compiler] abstract class CaseRefBuilder(ns: Path) + extends smithytranslate.compiler.internals.RefParser(ns) { + def unapply(sch: Schema[_]): Option[Either[ToSmithyError, DefId]] = Option(sch.get$ref).map(this.apply) - - private def handleRef( - uri: java.net.URI, - pathSegsIn: List[String], - fileName: String, - segments: Option[List[String]], - last: Option[String] - ): Either[ModelError, DefId] = { - val pathSegs = pathSegsIn.dropWhile(_ == ".") - - val ups = pathSegs.takeWhile(_ == "..").size - if (ns.length < ups + 1) { - // namespace contains the name of the file - val error = ModelError.Restriction(s"Ref $uri goes too far up") - Left(error) - } else { - // Removing as many ".." as needed to standardise the namespace - val nsPrefix = ns.toChain.toList.dropRight(ups + 1) - val nsPrefix2 = pathSegs.drop(ups) - val splitName = fileName.split('.') - val nsLastPart = - if (splitName.size > 1) splitName.dropRight(1).mkString(".") - else fileName - val fullNs = - Namespace((nsPrefix ++ nsPrefix2 :+ nsLastPart)) - val name = (segments, last) match { - case (Some(seg), Some(l)) => - NonEmptyChain - .fromSeq(seg.map(s => Segment.Arbitrary(CIString(s)))) - .map(Name(_)) - .map(_ ++ Name.derived(l)) - .getOrElse(Name.derived(l)) - case _ => Name.derived(nsLastPart) - } - Right(DefId(fullNs, name)) - } - } - - def apply(ref: String): Either[ModelError, DefId] = - scala.util - .Try(java.net.URI.create(ref)) - .toEither - .leftMap(_ => ModelError.BadRef(ref)) - .flatMap(uri => - Option(uri.getScheme()) match { - case Some("file") => Right(uri) - case None => Right(uri) - case Some(_) => Left(ModelError.BadRef(ref)) - } - ) - .flatMap { uri => - (uri.getPath(), uri.getFragment()) match { - case ("", NonEmptySegments(segments, last)) => - val n = Namespace(ns.toChain.toList) - val name = - NonEmptyChain - .fromSeq( - segments.map(s => Segment.Arbitrary(CIString(s))) - ) - .map(Name(_)) - .map(_ ++ Name.derived(last)) - .getOrElse(Name.derived(last)) - Right(DefId(n, name)) - case (NonEmptySegments(pathSegsIn, fileName), null) => - handleRef(uri, pathSegsIn, fileName, None, None) - case ( - NonEmptySegments(pathSegsIn, fileName), - NonEmptySegments(segments, last) - ) => - handleRef(uri, pathSegsIn, fileName, Some(segments), Some(last)) - } - } } diff --git a/modules/openapi/src/internals/HeadersToParams.scala b/modules/openapi/src/internals/HeadersToParams.scala index a85d6a0..f40df3b 100644 --- a/modules/openapi/src/internals/HeadersToParams.scala +++ b/modules/openapi/src/internals/HeadersToParams.scala @@ -13,12 +13,15 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi import io.swagger.v3.oas.models.headers.Header -import smithytranslate.openapi.internals.GetExtensions.HasExtensions +import GetExtensions.HasExtensions -object HeadersToParams extends (Iterable[(String, Header)] => Vector[Param]) { +private[openapi] object HeadersToParams + extends (Iterable[(String, Header)] => Vector[Param]) { def apply( headerMap: Iterable[(String, Header)] diff --git a/modules/openapi/src/internals/HttpMessageInfo.scala b/modules/openapi/src/internals/HttpMessageInfo.scala index 0f63848..932c0b4 100644 --- a/modules/openapi/src/internals/HttpMessageInfo.scala +++ b/modules/openapi/src/internals/HttpMessageInfo.scala @@ -13,9 +13,13 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi -case class HttpMessageInfo( +import smithytranslate.compiler.internals._ + +private[openapi] case class HttpMessageInfo( name: Name, params: Vector[Param], hints: List[Hint] diff --git a/modules/openapi/src/internals/Local.scala b/modules/openapi/src/internals/Local.scala index 10618e8..588321c 100644 --- a/modules/openapi/src/internals/Local.scala +++ b/modules/openapi/src/internals/Local.scala @@ -13,12 +13,14 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi import cats.data.NonEmptyChain import io.swagger.v3.oas.models.media.Schema -case class Local( +private[openapi] case class Local( context: Context, schema: Schema[_] ) { @@ -33,7 +35,7 @@ case class Local( } -object Local { +private[openapi] object Local { def apply(path: Name, schema: Schema[_]): Local = Local(Context(path, Nil, Nil), schema) diff --git a/modules/openapi/src/internals/OpenApiToIModel.scala b/modules/openapi/src/internals/OpenApiToIModel.scala index b74b09d..c795c5b 100644 --- a/modules/openapi/src/internals/OpenApiToIModel.scala +++ b/modules/openapi/src/internals/OpenApiToIModel.scala @@ -13,8 +13,9 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler package internals +package openapi import cats.Parallel import cats.mtl.Tell @@ -24,19 +25,19 @@ import io.swagger.v3.oas.models.media._ import scala.jdk.CollectionConverters._ import cats.data._ import Primitive._ +import ToSmithyError._ import cats.syntax.all._ -import ModelError._ import cats.Monad import org.typelevel.ci._ -import smithytranslate.openapi.internals.GetExtensions.HasExtensions +import GetExtensions.HasExtensions -object OpenApiToIModel { +private[compiler] object OpenApiToIModel { def compile( namespace: Path, openAPI: Either[List[String], OpenAPI] - ): (Chain[ModelError], IModel) = { - type ErrorLayer[A] = Writer[Chain[ModelError], A] + ): (Chain[ToSmithyError], IModel) = { + type ErrorLayer[A] = Writer[Chain[ToSmithyError], A] type WriterLayer[A] = WriterT[ErrorLayer, Chain[Either[Suppression, Definition]], A] val (errors, (data, _)) = @@ -55,176 +56,12 @@ object OpenApiToIModel { val parser = new OpenApiToIModel[F](namespace, openApi) parser.recordAll case Left(errors) => - Tell.tell(Chain.one((ModelError.OpenApiParseError(namespace, errors)))) + Tell.tell(Chain.one((ToSmithyError.OpenApiParseError(namespace, errors)))) } } - - final class OpenApiFolder[F[_]: Parallel: TellShape: TellError]( - namespace: Path - ) { - implicit val F: Monad[F] = Parallel[F].monad - - def id(context: Context): DefId = { - DefId(Namespace(namespace.toList), context.path) - } - - def errorId(context: Context): DefId = { - DefId(errorNamespace, context.path) - } - - private def errorNamespace: Namespace = Namespace(List("error")) - - private def recordError(e: ModelError): F[Unit] = - Tell.tell(Chain.one(e)) - - private def recordDef(definition: Definition): F[Unit] = - Tell.tell(Chain.one(Right(definition))) - - def std(name: String, hints: Hint*) = - ( - DefId( - Namespace(List("smithy", "api")), - Name.stdLib(name) - ), - hints.toList - ) - def smithyTranslate(name: String, hints: Hint*) = - ( - DefId(Namespace(List("smithytranslate")), Name.stdLib(name)), - hints.toList - ) - def alloy(name: String, hints: Hint*) = - ( - DefId(Namespace(List("alloy")), Name.stdLib(name)), - hints.toList - ) - - // format: off - def idFromPrimitive(primitive: Primitive): (DefId, List[Hint]) = - primitive match { - case PInt => std("Integer") - case PBoolean => std("Boolean") - case PString => std("String") - case PLong => std("Long") - case PByte => std("Byte") - case PFloat => std("Float") - case PDouble => std("Double") - case PShort => std("Short") - case PBytes => std("Blob") - case PFreeForm => std("Document") - case PUUID => alloy("UUID") - case PDate => std("String", Hint.Timestamp(TimestampFormat.SimpleDate)) - case PDateTime => std("Timestamp", Hint.Timestamp(TimestampFormat.DateTime)) - case PTimestamp => std("Timestamp") - } - // format: on - - /** Folds one layer into a type, recording definitions into the monadic tell - * as we go - */ - def fold(layer: OpenApiPattern[DefId]): F[DefId] = { - layer.context.errors.traverse(recordError) *> - (layer match { - case OpenApiPrimitive(context, primitive) => - val ntId = id(context) - val (target, hints) = idFromPrimitive(primitive) - val nt = Newtype(ntId, target, context.hints ++ hints) - recordDef(nt).as(ntId) - - case OpenApiRef(context, target) => - val ntId = id(context) - val nt = Newtype(ntId, target, context.hints) - recordDef(nt).as(ntId) - - case OpenApiEnum(context, values) => - val defId = id(context) - recordDef( - Enumeration(defId, values.filterNot(_ == null), context.hints) - ).as(defId) - - case OpenApiNull(context) => - val ntId = id(context) - val target = - DefId(Namespace(List("smithytranslate")), Name.stdLib("Null")) - val nt = Newtype(ntId, target, context.hints) - recordDef(nt).as(ntId) - - case OpenApiMap(context, itemType) => - val defId = id(context) - val (key, _) = idFromPrimitive(Primitive.PString) - val definition = MapDef(defId, key, itemType, context.hints) - recordDef(definition).as(defId) - - case OpenApiArray(context, itemType) => - val defId = id(context) - recordDef(ListDef(defId, itemType, context.hints)).as(defId) - - case OpenApiSet(context, itemType) => - val defId = id(context) - recordDef(SetDef(defId, itemType, context.hints)).as(defId) - - case OpenApiAllOf(context, parentTypes) => - val shapeId = id(context) - F.pure(Structure(shapeId, Vector.empty, parentTypes, context.hints)) - .flatTap(recordDef) - .as(shapeId) - - case OpenApiOneOf(context, alternatives, unionKind) => - val shapeId = id(context) - alternatives - .parTraverse { case (hints, tpe @ DefId(_, name)) => - val n = hints - .collectFirst { case Hint.ContentTypeLabel(l) => - StringUtil.toCamelCase(l) - } - .getOrElse(name.segments.last.value) - val altId = - MemberId(shapeId, Segment.Derived(CIString(n.toString))) - val alt = Alternative(altId, tpe, hints) - F.pure(alt) - } - .flatTap(alts => - recordDef(Union(shapeId, alts, unionKind, context.hints)) - ) - .as(shapeId) - - case OpenApiObject(context, items) => - val shapeId = id(context) - items - .map { case ((name, required), tpe) => - val fieldId = MemberId(shapeId, Segment.Derived(CIString(name))) - val hints = if (required) List(Hint.Required) else List.empty - Field(fieldId, tpe, hints) - } - .pure[F] - .flatTap { fields => - recordDef( - Structure(shapeId, fields, Vector.empty, context.hints) - ) - } - .as(shapeId) - - case OpenApiShortStop(context, error) => - // When an error was encountered during the unfold, we materialise it - // as an empty shape in the `error` namespace - val shapeId = errorId(context) - val definition = - Structure( - shapeId, - Vector.empty, - Vector.empty, - context.hints :+ Hint.ErrorMessage(error.getMessage) - ) - recordError(error) *> - recordDef(definition) *> - F.pure(shapeId) - }) - } - } - } -private class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( +private[openapi] class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( namespace: Path, openApi: OpenAPI ) { @@ -349,18 +186,26 @@ private class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( private def recordSecurityErrors: F[Unit] = securityErrors.traverse(recordError).void - private def fold = new OpenApiToIModel.OpenApiFolder[F](namespace).fold _ + private def fold = new PatternFolder[F](namespace).fold _ private def refoldOne(start: Local): F[DefId] = { // Refolding each top value under openapi's "component/schema" // into a type, recording definitions using Tell as we go, during // the collapse (fold). def unfoldAndAddExts(local: Local) = - unfold(local).map(GetExtensions.transformPattern(local)) + unfold(local).map(transformPattern(local)) recursion.refoldPar(unfoldAndAddExts _, fold)(start) } + private def transformPattern[A]( + local: Local + ): OpenApiPattern[A] => OpenApiPattern[A] = { + val maybeHints = GetExtensions.from(HasExtensions.unsafeFrom(local.schema)) + (pattern: OpenApiPattern[A]) => + pattern.mapContext(_.addHints(maybeHints, retainTopLevel = true)) + } + private def recordService(opDefIds: Vector[DefId]): F[Unit] = { val security = Option(openApi.getSecurity()) @@ -420,7 +265,7 @@ private class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( .traverse { i => val errorMessage = s"Multiple success responses are not supported. Found status code ${i.code} when ${output.code} was already recorded" - recordError(ModelError.Restriction(errorMessage)) *> + recordError(ToSmithyError.Restriction(errorMessage)) *> recordRefOrMessage( i.refOrMessage.map(m => m.copy(hints = m.hints :+ Hint.ErrorMessage(errorMessage)) @@ -531,7 +376,7 @@ private class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( private def getRangeTrait( local: Local, prim: Primitive - ): (Option[Hint], Option[ModelError]) = { + ): (Option[Hint], Option[ToSmithyError]) = { val minR = Option(local.schema.getMinimum()).map(BigDecimal(_)) val maxR = Option(local.schema.getMaximum()).map(BigDecimal(_)) val range = @@ -563,7 +408,7 @@ private class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( val primType = prim.toString.drop("P".length()) val errorMessage = s"Unable to automatically account for exclusiveMin/Max on decimal type $primType" - (r, Some(ModelError.Restriction(errorMessage))) + (r, Some(ToSmithyError.Restriction(errorMessage))) } }.unzip (updatedRange, error.flatten) @@ -807,7 +652,7 @@ private class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( // Utils // /////////////////////////////////////////////////////////////////////////// - private def recordError(e: ModelError): F[Unit] = + private def recordError(e: ToSmithyError): F[Unit] = Tell.tell(Chain.one(e)) private def recordDef(definition: Definition): F[Unit] = diff --git a/modules/openapi/src/internals/OperationInfo.scala b/modules/openapi/src/internals/OperationInfo.scala index a4087bd..da61fda 100644 --- a/modules/openapi/src/internals/OperationInfo.scala +++ b/modules/openapi/src/internals/OperationInfo.scala @@ -13,9 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi -final case class OperationInfo( +private[openapi] final case class OperationInfo( name: Name, method: HttpMethod, path: String, diff --git a/modules/openapi/src/internals/Output.scala b/modules/openapi/src/internals/Output.scala index c7f0656..0034629 100644 --- a/modules/openapi/src/internals/Output.scala +++ b/modules/openapi/src/internals/Output.scala @@ -13,9 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi -final case class Output( +private[openapi] final case class Output( code: Int, refOrMessage: RefOr[HttpMessageInfo], hints: List[Hint] diff --git a/modules/openapi/src/internals/Param.scala b/modules/openapi/src/internals/Param.scala index db6bcec..71abddc 100644 --- a/modules/openapi/src/internals/Param.scala +++ b/modules/openapi/src/internals/Param.scala @@ -13,11 +13,12 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler.internals.openapi +import smithytranslate.compiler.internals._ import io.swagger.v3.oas.models.media.Schema -final case class Param( +private[openapi] final case class Param( name: String, refOrSchema: RefOr[Schema[_]], hints: List[Hint] diff --git a/modules/openapi/src/internals/ParseOperations.scala b/modules/openapi/src/internals/ParseOperations.scala index 05186ba..8514f00 100644 --- a/modules/openapi/src/internals/ParseOperations.scala +++ b/modules/openapi/src/internals/ParseOperations.scala @@ -13,22 +13,22 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler package internals +package openapi import cats.data.{Chain, NonEmptyChain} import cats.syntax.all._ import io.swagger.v3.oas.models import io.swagger.v3.oas.models.OpenAPI import org.typelevel.ci._ -import smithytranslate.openapi.internals.GetExtensions.HasExtensions +import GetExtensions.HasExtensions import scala.jdk.CollectionConverters._ -import smithytranslate.openapi.internals.Hint.Header import scala.collection.compat._ -object ParseOperations +private[openapi] object ParseOperations extends ( ( SecuritySchemes, @@ -77,7 +77,7 @@ private class ParseOperationsImpl( val opsAndErrors = paths .map { case (path, item) => - val allCommonParams: Vector[Either[ModelError, Param]] = + val allCommonParams: Vector[Either[ToSmithyError, Param]] = Option(item.getParameters()).toVector .flatMap(_.asScala.toVector) .map(getParam) @@ -162,7 +162,7 @@ private class ParseOperationsImpl( allParams .flatMap(_.hints) .exists { - case Header(name) => + case Hint.Header(name) => restrictedHeaders(CIString(name)) case _ => false } @@ -173,10 +173,10 @@ private class ParseOperationsImpl( hasGlobalSecurity: Boolean )(opInfo: IOpInfo): ParseOperationsResult = { import opInfo._ - val localParams: Vector[Either[ModelError, Param]] = getLocalInputParams(op) - val body: Vector[Either[ModelError, Param]] = + val localParams: Vector[Either[ToSmithyError, Param]] = getLocalInputParams(op) + val body: Vector[Either[ToSmithyError, Param]] = getRequestBodyParam(op).toVector.map(Right(_)) - val allInputParams: Vector[Either[ModelError, Param]] = + val allInputParams: Vector[Either[ToSmithyError, Param]] = opInfo.commonParams.map(Right(_)) ++ localParams ++ body val allValidInputParams = allInputParams.collect { case Right(p) => p } val allInputParamsErrors = allInputParams.collect { case Left(e) => e } @@ -218,7 +218,7 @@ private class ParseOperationsImpl( securitySchemes: SecuritySchemes, opInfo: IOpInfo, hasGlobalSecurity: Boolean - ): (List[ModelError], Option[Hint.Auth]) = { + ): (List[ToSmithyError], Option[Hint.Auth]) = { import opInfo._ val id = getOpId(opInfo) val security = @@ -241,7 +241,7 @@ private class ParseOperationsImpl( else None val errors = allSecurityKeys.collect { case s if s.size > 1 => - ModelError.Restriction( + ToSmithyError.Restriction( s"Operation ${id.segments.mkString_(".")} contains an unsupported security requirement: `$s`. " + s"Security schemes cannot be ANDed together. ${s.head} will be used and ${s.tail} will be ignored." ) @@ -280,8 +280,8 @@ private class ParseOperationsImpl( private def getParam( parameter: models.parameters.Parameter - ): Either[ModelError, Param] = { - val maybeResolvedParam: Either[ModelError, models.parameters.Parameter] = + ): Either[ToSmithyError, Param] = { + val maybeResolvedParam: Either[ToSmithyError, models.parameters.Parameter] = Option(parameter.get$ref()) .toLeft(parameter) .leftFlatMap(ref => @@ -321,7 +321,7 @@ private class ParseOperationsImpl( Param(name, refOrSchema, hints) } .toRight( - ModelError.ProcessingError( + ToSmithyError.ProcessingError( s"Parameter ${name} should have in defined as `query`, `path` or `header." ) ) @@ -330,7 +330,7 @@ private class ParseOperationsImpl( private def getLocalInputParams( op: models.Operation - ): Vector[Either[ModelError, Param]] = { + ): Vector[Either[ToSmithyError, Param]] = { Option(op.getParameters()).toVector .flatMap( _.asScala diff --git a/modules/openapi/src/internals/ParseOperationsResult.scala b/modules/openapi/src/internals/ParseOperationsResult.scala index 1138aea..0415352 100644 --- a/modules/openapi/src/internals/ParseOperationsResult.scala +++ b/modules/openapi/src/internals/ParseOperationsResult.scala @@ -13,27 +13,28 @@ * limitations under the License. */ -package smithytranslate.openapi.internals +package smithytranslate.compiler +package internals +package openapi import cats.kernel.Monoid -import smithytranslate.openapi.ModelError -case class SuppressionFor(id: String, reason: String) +private[openapi] case class SuppressionFor(id: String, reason: String) extends (Namespace => Suppression) { def apply(namespace: Namespace): Suppression = Suppression(id, namespace, reason) } -final case class ParseOperationsResult( - errors: List[ModelError], +private[openapi] final case class ParseOperationsResult( + errors: List[ToSmithyError], results: Vector[OperationInfo], suppressions: Vector[SuppressionFor] ) { - def addErrors(err: Seq[ModelError]): ParseOperationsResult = + def addErrors(err: Seq[ToSmithyError]): ParseOperationsResult = this.copy(errors = this.errors ++ err) } -object ParseOperationsResult { +private[openapi] object ParseOperationsResult { implicit val monoid: Monoid[ParseOperationsResult] = new Monoid[ParseOperationsResult] { def empty: ParseOperationsResult = diff --git a/modules/openapi/src/internals/ParseSecuritySchemes.scala b/modules/openapi/src/internals/ParseSecuritySchemes.scala index 2b72612..c4bf767 100644 --- a/modules/openapi/src/internals/ParseSecuritySchemes.scala +++ b/modules/openapi/src/internals/ParseSecuritySchemes.scala @@ -13,8 +13,9 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler package internals +package openapi import io.swagger.v3.oas.models.OpenAPI import scala.jdk.CollectionConverters._ @@ -22,9 +23,9 @@ import io.swagger.v3.oas.models.security.SecurityScheme.Type._ import io.swagger.v3.oas.models.security.SecurityScheme.In._ import cats.syntax.all._ -object ParseSecuritySchemes - extends (OpenAPI => (List[ModelError], SecuritySchemes)) { - def apply(o: OpenAPI): (List[ModelError], SecuritySchemes) = +private[openapi] object ParseSecuritySchemes + extends (OpenAPI => (List[ToSmithyError], SecuritySchemes)) { + def apply(o: OpenAPI): (List[ToSmithyError], SecuritySchemes) = Option(o.getComponents()) .flatMap(c => Option(c.getSecuritySchemes()).map(_.asScala)) .getOrElse(Map.empty) @@ -35,7 +36,7 @@ object ParseSecuritySchemes ss.getIn() match { case COOKIE => error( - ModelError.Restriction( + ToSmithyError.Restriction( "Cookie is not a supported ApiKey location." ) ) @@ -62,26 +63,26 @@ object ParseSecuritySchemes success(name -> SecurityScheme.BasicAuth) case format => error( - ModelError.Restriction( + ToSmithyError.Restriction( s"$format is not a supported format of the http security scheme." ) ) } case OAUTH2 => error( - ModelError.Restriction( + ToSmithyError.Restriction( "OAuth2 is not a supported security scheme." ) ) case OPENIDCONNECT => error( - ModelError.Restriction( + ToSmithyError.Restriction( "OpenIdConnect is not a supported security scheme." ) ) case MUTUALTLS => error( - ModelError.Restriction( + ToSmithyError.Restriction( "MutualTLS is not a supported security scheme." ) ) @@ -92,11 +93,11 @@ object ParseSecuritySchemes private def success( in: (String, SecurityScheme) - ): (List[ModelError], Option[(String, SecurityScheme)]) = + ): (List[ToSmithyError], Option[(String, SecurityScheme)]) = (Nil, Some(in)) private def error( - modelError: ModelError - ): (List[ModelError], Option[(String, SecurityScheme)]) = + modelError: ToSmithyError + ): (List[ToSmithyError], Option[(String, SecurityScheme)]) = (List(modelError), None) } diff --git a/modules/openapi/src/internals/RefParser.scala b/modules/openapi/src/internals/RefParser.scala index f819141..f9b7c97 100644 --- a/modules/openapi/src/internals/RefParser.scala +++ b/modules/openapi/src/internals/RefParser.scala @@ -13,11 +13,10 @@ * limitations under the License. */ -package smithytranslate.openapi.internals - +package smithytranslate.compiler.internals.openapi import io.swagger.v3.oas.models.media.Schema -object RefParser { +private[openapi] object RefParser { def toSchema(ref: String): Schema[_] = { val sch = new Schema() diff --git a/modules/openapi/tests/src/AllOfSpec.scala b/modules/openapi/tests/src/AllOfSpec.scala index b9b5c49..d7af347 100644 --- a/modules/openapi/tests/src/AllOfSpec.scala +++ b/modules/openapi/tests/src/AllOfSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class AllOfSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/ConstraintSpec.scala b/modules/openapi/tests/src/ConstraintSpec.scala index 12f332f..1312a8c 100644 --- a/modules/openapi/tests/src/ConstraintSpec.scala +++ b/modules/openapi/tests/src/ConstraintSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import cats.data.NonEmptyList import software.amazon.smithy.model.shapes.LongShape @@ -21,6 +21,9 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.traits.RangeTrait import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.FloatShape +import smithytranslate.compiler.SmithyVersion +import smithytranslate.compiler.ToSmithyResult +import smithytranslate.compiler.ToSmithyError final class ConstraintSpec extends munit.FunSuite { test("length - string") { @@ -239,15 +242,15 @@ final class ConstraintSpec extends munit.FunSuite { openapiString, expectedModel, None, - OpenApiCompiler.SmithyVersion.Two + SmithyVersion.Two ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expected ) = TestUtils.runConversion(input) val expectedErrors = List( - ModelError.Restriction( + ToSmithyError.Restriction( "Unable to automatically account for exclusiveMin/Max on decimal type Float" ) ) diff --git a/modules/openapi/tests/src/DebugSpec.scala b/modules/openapi/tests/src/DebugSpec.scala index 659a8a7..28ca084 100644 --- a/modules/openapi/tests/src/DebugSpec.scala +++ b/modules/openapi/tests/src/DebugSpec.scala @@ -13,12 +13,15 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import cats.data.NonEmptyList -import smithytranslate.openapi.OpenApiCompiler.Failure -import smithytranslate.openapi.OpenApiCompiler.Success +import smithytranslate.compiler.ToSmithyResult.Failure +import smithytranslate.compiler.ToSmithyResult.Success import munit.Location +import smithytranslate.compiler.ToSmithyCompilerOptions +import smithytranslate.compiler.ToSmithyError +import smithytranslate.compiler.FileContents final class DebugSpec extends munit.FunSuite { @@ -27,25 +30,29 @@ final class DebugSpec extends munit.FunSuite { .fromResource(fileName) .getLines() .mkString("\n") - val inputs = Seq((NonEmptyList.of(fileName), content)) - OpenApiCompiler.parseAndCompile( - OpenApiCompiler.Options( - useVerboseNames = false, - validateInput = false, - validateOutput = true, - List.empty, - useEnumTraitSyntax = false, - debug - ), - inputs: _* + + val options = ToSmithyCompilerOptions( + useVerboseNames = false, + validateInput = false, + validateOutput = true, + List.empty, + useEnumTraitSyntax = false, + debug + ) + + val input = OpenApiCompilerInput.UnparsedSpecs( + List( + FileContents(NonEmptyList.of(fileName), content) + ) ) + OpenApiCompiler.compile(options, input) } private def testFilteredErrors(debug: Boolean, expectedCount: Int)(implicit loc: Location ) = { load("issue-23.json", debug) match { - case Failure(ModelError.SmithyValidationFailed(events), _) => + case Failure(ToSmithyError.SmithyValidationFailed(events), _) => assertEquals(events.size, expectedCount) case Failure(cause, _) => fail( diff --git a/modules/openapi/tests/src/DocumentSpec.scala b/modules/openapi/tests/src/DocumentSpec.scala index 34305f1..f340cfe 100644 --- a/modules/openapi/tests/src/DocumentSpec.scala +++ b/modules/openapi/tests/src/DocumentSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class DocumentSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/EnumSpec.scala b/modules/openapi/tests/src/EnumSpec.scala index f3b238c..186faa8 100644 --- a/modules/openapi/tests/src/EnumSpec.scala +++ b/modules/openapi/tests/src/EnumSpec.scala @@ -13,9 +13,10 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi + +import smithytranslate.compiler.SmithyVersion -import smithytranslate.openapi.OpenApiCompiler.SmithyVersion final class EnumSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/ExamplesSpec.scala b/modules/openapi/tests/src/ExamplesSpec.scala index 8fa1f4c..ea27479 100644 --- a/modules/openapi/tests/src/ExamplesSpec.scala +++ b/modules/openapi/tests/src/ExamplesSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class ExamplesSpec extends munit.FunSuite { @@ -230,7 +230,7 @@ final class ExamplesSpec extends munit.FunSuite { | |use alloy#dataExamples | - |@dataExamples([{json: + |@dataExamples([{json: | { | foo: "bar" | } diff --git a/modules/openapi/tests/src/ExtensionsSpec.scala b/modules/openapi/tests/src/ExtensionsSpec.scala index d281376..533d5af 100644 --- a/modules/openapi/tests/src/ExtensionsSpec.scala +++ b/modules/openapi/tests/src/ExtensionsSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class ExtensionsSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/ListSpec.scala b/modules/openapi/tests/src/ListSpec.scala index 54e16f8..7c88902 100644 --- a/modules/openapi/tests/src/ListSpec.scala +++ b/modules/openapi/tests/src/ListSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class ListSpec extends munit.FunSuite { test("lists") { diff --git a/modules/openapi/tests/src/MapSpec.scala b/modules/openapi/tests/src/MapSpec.scala index caf6b5d..a2fa4ca 100644 --- a/modules/openapi/tests/src/MapSpec.scala +++ b/modules/openapi/tests/src/MapSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class MapSpec extends munit.FunSuite { test("maps") { diff --git a/modules/openapi/tests/src/MultiFileSpec.scala b/modules/openapi/tests/src/MultiFileSpec.scala index 63a3106..205ec69 100644 --- a/modules/openapi/tests/src/MultiFileSpec.scala +++ b/modules/openapi/tests/src/MultiFileSpec.scala @@ -13,9 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import cats.data.NonEmptyList +import smithytranslate.compiler.ToSmithyResult +import smithytranslate.compiler.ToSmithyError final class MultiFileSpec extends munit.FunSuite { /* . @@ -397,12 +399,12 @@ final class MultiFileSpec extends munit.FunSuite { expectedBin ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expectedModel ) = TestUtils.runConversion(inOne, inTwo) val expectedErrors = List( - ModelError.Restriction( + ToSmithyError.Restriction( "Ref ../../../bar.yaml#/components/schemas/Test goes too far up" ) ) diff --git a/modules/openapi/tests/src/OpenapiV2Spec.scala b/modules/openapi/tests/src/OpenapiV2Spec.scala index 8ecdd5a..7f2f3e9 100644 --- a/modules/openapi/tests/src/OpenapiV2Spec.scala +++ b/modules/openapi/tests/src/OpenapiV2Spec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class OpenapiV2Spec extends munit.FunSuite { test("operation - simple response") { diff --git a/modules/openapi/tests/src/OperationContentTypesSpec.scala b/modules/openapi/tests/src/OperationContentTypesSpec.scala index e6b2935..1b14b9d 100644 --- a/modules/openapi/tests/src/OperationContentTypesSpec.scala +++ b/modules/openapi/tests/src/OperationContentTypesSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class OperationContentTypesSpec extends munit.FunSuite { @@ -274,10 +274,10 @@ final class OperationContentTypesSpec extends munit.FunSuite { | '200': | content: | application/json: - | schema: + | schema: | $ref: '#/components/schemas/Employee' | application/xml: - | schema: + | schema: | $ref: '#/components/schemas/Employee' |components: | schemas: @@ -288,7 +288,7 @@ final class OperationContentTypesSpec extends munit.FunSuite { | type: integer | name: | type: string - | fullTime: + | fullTime: | type: boolean |""".stripMargin diff --git a/modules/openapi/tests/src/OperationEnumSpec.scala b/modules/openapi/tests/src/OperationEnumSpec.scala index ae733a7..bcbbe03 100644 --- a/modules/openapi/tests/src/OperationEnumSpec.scala +++ b/modules/openapi/tests/src/OperationEnumSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class OperationEnumSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/OperationErrorSpec.scala b/modules/openapi/tests/src/OperationErrorSpec.scala index 028d2d6..7c805e1 100644 --- a/modules/openapi/tests/src/OperationErrorSpec.scala +++ b/modules/openapi/tests/src/OperationErrorSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class OperationErrorSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/OperationHeaderSpec.scala b/modules/openapi/tests/src/OperationHeaderSpec.scala index aef26fd..37f6a03 100644 --- a/modules/openapi/tests/src/OperationHeaderSpec.scala +++ b/modules/openapi/tests/src/OperationHeaderSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class OperationHeaderSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/OperationMultipleSuccessSpec.scala b/modules/openapi/tests/src/OperationMultipleSuccessSpec.scala index f52af28..4e9f7e4 100644 --- a/modules/openapi/tests/src/OperationMultipleSuccessSpec.scala +++ b/modules/openapi/tests/src/OperationMultipleSuccessSpec.scala @@ -13,9 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import cats.data.NonEmptyList +import smithytranslate.compiler.ToSmithyResult +import smithytranslate.compiler.ToSmithyError final class OperationMultipleSuccessSpec extends munit.FunSuite { @@ -105,12 +107,12 @@ final class OperationMultipleSuccessSpec extends munit.FunSuite { Some(expectedError) ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expectedModel ) = TestUtils.runConversion(input) val expectedErrors = List( - ModelError.Restriction( + ToSmithyError.Restriction( "Multiple success responses are not supported. Found status code 202 when 200 was already recorded" ) ) @@ -205,12 +207,12 @@ final class OperationMultipleSuccessSpec extends munit.FunSuite { None // no error namespace shapes because `alsoOkay` is defined as a reusable response ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expectedModel ) = TestUtils.runConversion(input) val expectedErrors = List( - ModelError.Restriction( + ToSmithyError.Restriction( "Multiple success responses are not supported. Found status code 202 when 200 was already recorded" ) ) diff --git a/modules/openapi/tests/src/OperationSpec.scala b/modules/openapi/tests/src/OperationSpec.scala index 8c88d66..eb5aa85 100644 --- a/modules/openapi/tests/src/OperationSpec.scala +++ b/modules/openapi/tests/src/OperationSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class OperationSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/OutputValidationSpec.scala b/modules/openapi/tests/src/OutputValidationSpec.scala index b3a552c..152baee 100644 --- a/modules/openapi/tests/src/OutputValidationSpec.scala +++ b/modules/openapi/tests/src/OutputValidationSpec.scala @@ -13,16 +13,19 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import cats.data.NonEmptyList -import smithytranslate.openapi.OpenApiCompiler.Failure -import smithytranslate.openapi.OpenApiCompiler.Success +import smithytranslate.compiler.ToSmithyResult.Failure +import smithytranslate.compiler.ToSmithyResult.Success +import smithytranslate.compiler.ToSmithyCompilerOptions +import smithytranslate.compiler.ToSmithyError +import smithytranslate.compiler.FileContents final class OutputValidationSpec extends munit.FunSuite { test("Output should be validated when specified") { - val input = """|openapi: '3.0.' + val spec = """|openapi: '3.0.' |info: | title: test | version: '1.0' @@ -38,9 +41,13 @@ final class OutputValidationSpec extends munit.FunSuite { | type: object |""".stripMargin - val inputs = Seq((NonEmptyList.of("input.yaml"), input)) - def convert(validateOutput: Boolean) = OpenApiCompiler.parseAndCompile( - OpenApiCompiler.Options( + val input = OpenApiCompilerInput.UnparsedSpecs( + List( + FileContents(NonEmptyList.of("input.yaml"), spec) + ) + ) + def convert(validateOutput: Boolean) = OpenApiCompiler.compile( + ToSmithyCompilerOptions( useVerboseNames = false, validateInput = false, validateOutput = validateOutput, @@ -48,7 +55,7 @@ final class OutputValidationSpec extends munit.FunSuite { useEnumTraitSyntax = false, debug = false ), - inputs: _* + input ) val resultExpectingSuccess = convert(validateOutput = false) @@ -56,7 +63,7 @@ final class OutputValidationSpec extends munit.FunSuite { val resultExpectingFailure = convert(validateOutput = true) resultExpectingFailure match { - case Failure(ModelError.SmithyValidationFailed(events), _) => + case Failure(ToSmithyError.SmithyValidationFailed(events), _) => // Expecting a failure indicating that the "test" operation is invalid due to not having // an input member matching the `{test}` path segment. assertEquals(events.size, 1) diff --git a/modules/openapi/tests/src/ParameterSpec.scala b/modules/openapi/tests/src/ParameterSpec.scala index 5939341..3504eec 100644 --- a/modules/openapi/tests/src/ParameterSpec.scala +++ b/modules/openapi/tests/src/ParameterSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class ParameterSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/PrimitiveSpec.scala b/modules/openapi/tests/src/PrimitiveSpec.scala index 236d660..a03ba0a 100644 --- a/modules/openapi/tests/src/PrimitiveSpec.scala +++ b/modules/openapi/tests/src/PrimitiveSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class PrimitiveSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/SecuritySchemesSpec.scala b/modules/openapi/tests/src/SecuritySchemesSpec.scala index 1b7daeb..489fd2d 100644 --- a/modules/openapi/tests/src/SecuritySchemesSpec.scala +++ b/modules/openapi/tests/src/SecuritySchemesSpec.scala @@ -13,9 +13,11 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import cats.data.NonEmptyList +import smithytranslate.compiler.ToSmithyResult +import smithytranslate.compiler.ToSmithyError final class SecuritySchemesSpec extends munit.FunSuite { @@ -597,11 +599,11 @@ final class SecuritySchemesSpec extends munit.FunSuite { None ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expected ) = TestUtils.runConversion(input) - val expectedError = ModelError.Restriction( + val expectedError = ToSmithyError.Restriction( "Operation testOperationId contains an unsupported security requirement: `List(BasicAuth, BearerAuth)`. " + "Security schemes cannot be ANDed together. BasicAuth will be used and List(BearerAuth) will be ignored." ) @@ -758,11 +760,11 @@ final class SecuritySchemesSpec extends munit.FunSuite { None ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expected ) = TestUtils.runConversion(input) - val expectedError = ModelError.Restriction( + val expectedError = ToSmithyError.Restriction( "Operation testOperationId contains an unsupported security requirement: `List(BasicAuth, BearerAuth)`. " + "Security schemes cannot be ANDed together. BasicAuth will be used and List(BearerAuth) will be ignored." ) @@ -853,15 +855,15 @@ final class SecuritySchemesSpec extends munit.FunSuite { None ) val TestUtils.ConversionResult( - OpenApiCompiler.Success(errors, output), + ToSmithyResult.Success(errors, output), expected ) = TestUtils.runConversion(input) val expectedErrors = List( - ModelError.Restriction( + ToSmithyError.Restriction( "OpenIdConnect is not a supported security scheme." ), - ModelError.Restriction("OAuth2 is not a supported security scheme.") + ToSmithyError.Restriction("OAuth2 is not a supported security scheme.") ) assertEquals(output, expected) assertEquals(errors, expectedErrors) diff --git a/modules/openapi/tests/src/SetSpec.scala b/modules/openapi/tests/src/SetSpec.scala index 9a6cc22..0964347 100644 --- a/modules/openapi/tests/src/SetSpec.scala +++ b/modules/openapi/tests/src/SetSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class SetSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/SpecialTypesSpec.scala b/modules/openapi/tests/src/SpecialTypesSpec.scala index 614b221..9853e33 100644 --- a/modules/openapi/tests/src/SpecialTypesSpec.scala +++ b/modules/openapi/tests/src/SpecialTypesSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class SpecialTypesSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/StructureSpec.scala b/modules/openapi/tests/src/StructureSpec.scala index 5ec965e..c64dc7f 100644 --- a/modules/openapi/tests/src/StructureSpec.scala +++ b/modules/openapi/tests/src/StructureSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class StructureSpec extends munit.FunSuite { test("structures") { diff --git a/modules/openapi/tests/src/TagsSpec.scala b/modules/openapi/tests/src/TagsSpec.scala index f0ab45d..4d055b2 100644 --- a/modules/openapi/tests/src/TagsSpec.scala +++ b/modules/openapi/tests/src/TagsSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class TagsSpec extends munit.FunSuite { diff --git a/modules/openapi/tests/src/TestUtils.scala b/modules/openapi/tests/src/TestUtils.scala index 8d0117d..ce1d979 100644 --- a/modules/openapi/tests/src/TestUtils.scala +++ b/modules/openapi/tests/src/TestUtils.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer @@ -25,14 +25,15 @@ import cats.syntax.all._ import munit.Location import cats.data.NonEmptyList import software.amazon.smithy.model.node._ -import smithytranslate.openapi.OpenApiCompiler.SmithyVersion -import smithytranslate.openapi.TestUtils.ExpectedOutput.StringOutput -import smithytranslate.openapi.TestUtils.ExpectedOutput.ModelOutput import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.traits.Trait import software.amazon.smithy.model.traits.BoxTrait import scala.compat.java8.FunctionConverters._ +import smithytranslate.compiler.SmithyVersion +import smithytranslate.compiler.ToSmithyResult +import smithytranslate.compiler.ToSmithyCompilerOptions +import smithytranslate.compiler.FileContents object TestUtils { @@ -83,7 +84,7 @@ object TestUtils { } final case class ConversionResult( - result: OpenApiCompiler.Result[ModelWrapper], + result: ToSmithyResult[ModelWrapper], expected: ModelWrapper ) @@ -91,11 +92,11 @@ object TestUtils { input0: ConversionTestInput, remaining: ConversionTestInput* ): ConversionResult = { - val inputs = input0 +: remaining + val inputs = (input0 +: remaining).toList val result = - OpenApiCompiler.parseAndCompile( - OpenApiCompiler.Options( + OpenApiCompiler.compile( + ToSmithyCompilerOptions( useVerboseNames = false, validateInput = false, validateOutput = false, @@ -103,7 +104,9 @@ object TestUtils { input0.smithyVersion == SmithyVersion.One, debug = true ), - inputs.map(i => i.filePath -> i.openapiSpec): _* + OpenApiCompilerInput.UnparsedSpecs( + inputs.map(i => FileContents(i.filePath, i.openapiSpec)) + ) ) val resultW = result.map(ModelWrapper(_)) @@ -113,13 +116,13 @@ object TestUtils { inputs.foreach { i => i.smithySpec match { - case StringOutput(str) => + case TestUtils.ExpectedOutput.StringOutput(str) => val name = i.filePath.mkString_("/") + ".smithy" val spec = s"""|$$version: "${i.smithyVersion}" | |${str}""".stripMargin assembler.addUnparsedModel(name, spec) - case ModelOutput(model) => + case TestUtils.ExpectedOutput.ModelOutput(model) => assembler.addModel(model) } i.errorSmithySpec.foreach( @@ -146,10 +149,10 @@ object TestUtils { ) res match { - case OpenApiCompiler.Failure(err, errors) => + case ToSmithyResult.Failure(err, errors) => errors.foreach(println) Assertions.fail("Validating model failed: ", err) - case OpenApiCompiler.Success(_, output) => + case ToSmithyResult.Success(_, output) => Assertions.assertEquals(output, expected) } } diff --git a/modules/openapi/tests/src/TimestampSpec.scala b/modules/openapi/tests/src/TimestampSpec.scala index 3f81f6f..2362070 100644 --- a/modules/openapi/tests/src/TimestampSpec.scala +++ b/modules/openapi/tests/src/TimestampSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class TimestampSpec extends munit.FunSuite { test("date-time") { diff --git a/modules/openapi/tests/src/UnionSpec.scala b/modules/openapi/tests/src/UnionSpec.scala index d5741d5..dc8a18c 100644 --- a/modules/openapi/tests/src/UnionSpec.scala +++ b/modules/openapi/tests/src/UnionSpec.scala @@ -13,7 +13,7 @@ * limitations under the License. */ -package smithytranslate.openapi +package smithytranslate.compiler.openapi final class UnionSpec extends munit.FunSuite { test("unions - primitive targets") { diff --git a/modules/readme-validator/src/Validator.scala b/modules/readme-validator/src/Validator.scala index 78f4663..7a9e622 100644 --- a/modules/readme-validator/src/Validator.scala +++ b/modules/readme-validator/src/Validator.scala @@ -18,10 +18,11 @@ import java.nio.file.Path import scala.jdk.CollectionConverters._ import scala.util.control.NoStackTrace import smithyproto.proto3.{Compiler => ProtoCompiler, ModelPreProcessor} -import smithytranslate.openapi.OpenApiCompiler +import smithytranslate.compiler._ +import smithytranslate.compiler.openapi._ +import smithytranslate.compiler.json_schema._ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer -import smithytranslate.json_schema.JsonSchemaCompiler object Validator { @@ -87,7 +88,7 @@ object Validator { | |$smithy""".stripMargin val options = - OpenApiCompiler.Options( + ToSmithyCompilerOptions( useVerboseNames = false, validateInput = true, validateOutput = true, @@ -96,16 +97,18 @@ object Validator { debug = false ) val result = - OpenApiCompiler.parseAndCompile( + OpenApiCompiler.compile( options, - (NonEmptyList.of(namespace), openapi) + OpenApiCompilerInput.UnparsedSpecs( + List(FileContents(NonEmptyList.of(namespace), openapi)) + ) ) result match { - case OpenApiCompiler.Failure(err, _) => + case ToSmithyResult.Failure(err, _) => List( ValidationError.UnableToProduceOutput(err.getMessage) ) - case OpenApiCompiler.Success(errors, expectedModel) => + case ToSmithyResult.Success(errors, expectedModel) => val actualModel = Model .assembler() .discoverModels() @@ -135,7 +138,7 @@ object Validator { | |$smithy""".stripMargin val options = - OpenApiCompiler.Options( + ToSmithyCompilerOptions( useVerboseNames = false, validateInput = true, validateOutput = true, @@ -144,16 +147,18 @@ object Validator { debug = false ) val result = - JsonSchemaCompiler.parseAndCompile( + JsonSchemaCompiler.compile( options, - (NonEmptyList.of(namespace), json) + JsonSchemaCompilerInput.UnparsedSpecs( + List(FileContents(NonEmptyList.of(namespace), json)) + ) ) result match { - case OpenApiCompiler.Failure(err, _) => + case ToSmithyResult.Failure(err, _) => List( ValidationError.UnableToProduceOutput(err.getMessage) ) - case OpenApiCompiler.Success(errors, expectedModel) => + case ToSmithyResult.Success(errors, expectedModel) => val actualModel = Model .assembler() .discoverModels()