Skip to content

Commit

Permalink
Add smithy build openapi support (#1492)
Browse files Browse the repository at this point in the history
This adds support for supplying a `smithy-build.json` during codegen, either by CLI or by either the SBT or Mill plugins. Initially, the only thing in this file that is supported is the OpenAPI plugin, but in future more features can be parsed and handled in codegen if so desired. Docs have been added that explain this.
  • Loading branch information
astridej authored May 6, 2024
1 parent b498331 commit 001bb47
Show file tree
Hide file tree
Showing 21 changed files with 534 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* Constraints applied to list or map members are now correctly rendered in the generated code.
* Fix an issue with duplicated entries in generated smithy-build.json file (#1491)
* Add support for passing custom OpenAPI config via a `smithy-build.json` file

# 0.18.16

Expand Down
16 changes: 13 additions & 3 deletions modules/codegen-cli/src/smithy4s/codegen/cli/CodegenCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ object CodegenCommand {
.map(_.toSet)
.orNone

val smithyBuildOpt: Opts[Option[os.Path]] =
Opts
.option[os.Path](
"smithy-build",
"Path of smithy-build.json file containing smithy build arguments"
)
.orNone

val options =
(
outputOpt,
Expand All @@ -105,11 +113,12 @@ object CodegenCommand {
dependenciesOpt,
transformersOpt,
localJarsOpt,
specsArgs
specsArgs,
smithyBuildOpt
)
.mapN {
// format: off
case (output, resourseOutput, skip, discoverModels, allowedNS, excludedNS, repositories, dependencies, transformers, localJars, specsArgs) =>
case (output, resourseOutput, skip, discoverModels, allowedNS, excludedNS, repositories, dependencies, transformers, localJars, specsArgs, smithyBuild) =>
// format: on
val dependenciesWithDefaults = {
import Defaults._
Expand All @@ -126,7 +135,8 @@ object CodegenCommand {
repositories.getOrElse(List.empty),
dependenciesWithDefaults,
transformers.getOrElse(List.empty),
localJars.getOrElse(List.empty)
localJars.getOrElse(List.empty),
smithyBuild
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ object CommandParsingSpec extends FunSuite {
repositories = Nil,
dependencies = defaultDependencies,
transformers = Nil,
localJars = Nil
localJars = Nil,
smithyBuild = None
)
)
)
Expand All @@ -60,6 +61,8 @@ object CommandParsingSpec extends FunSuite {
"scala",
"--skip",
"openapi",
"--smithy-build",
"smithy-build.json",
"--allowed-ns",
"name1,name2",
"--repositories",
Expand Down Expand Up @@ -94,7 +97,8 @@ object CommandParsingSpec extends FunSuite {
localJars = List(
os.pwd / "lib1.jar",
os.pwd / "lib2.jar"
)
),
smithyBuild = Some(os.pwd / "smithy-build.json")
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
lazy val root = (project in file("."))
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
scalaVersion := "2.13.13",
libraryDependencies ++= Seq(
"com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion.value
),
Compile / smithyBuild := Some(baseDirectory.value / "smithy-build.json"),
TaskKey[Unit]("checkOpenApi") := {
val resourceDir = (Compile / smithy4sResourceDir).value
val content =
IO.readLines(
resourceDir / "smithy4s.example.ObjectService.json"
).filter(_.trim().nonEmpty)
.mkString("")
.trim()
if (!content.contains("X-Bar") || !content.contains("3.1.0"))
sys.error("OpenAPI transformation was not applied")
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.8.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
sys.props.get("plugin.version") match {
case Some(x) =>
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % x)
case _ =>
sys.error(
"""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "1.0",
"plugins": {
"openapi": {
"service": "smithy4s.example#ObjectService",
"version": "3.1.0",
"substitutions": {
"X-Foo": "X-Bar"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace smithy4s.example

use alloy#simpleRestJson

@simpleRestJson
service ObjectService {
version: "1.0.0",
operations: [PutObject, GetObject]
}


@idempotent
@http(method: "PUT", uri: "/{bucketName}/{key}", code: 200)
operation PutObject {
input: PutObjectInput,
errors: [NoMoreSpace]
}

@readonly
@http(method: "GET", uri: "/{bucketName}/{key}", code: 200)
operation GetObject {
input: GetObjectInput,
output: GetObjectOutput
}

structure PutObjectInput {
// Sent in the URI label named "key".
@required
@httpLabel
key: String,

// Sent in the URI label named "bucketName".
@required
@httpLabel
bucketName: String,

// Sent in the X-Foo header
@httpHeader("X-Foo")
foo: String,

// Sent in the query string as paramName
@httpQuery("paramName")
someValue: String,

// Sent in the body
@httpPayload
@required
data: String
}

structure GetObjectInput {
// Sent in the URI label named "key".
@required
@httpLabel
key: String,

// Sent in the URI label named "bucketName".
@required
@httpLabel
bucketName: String,
}

structure GetObjectOutput {
@httpHeader("X-Size")
@required
size: Integer,
@httpPayload
data: String
}

union Foo {
int: Integer,
str: String
}

@error("server")
@httpError(507)
structure NoMoreSpace {
@required
message: String,
foo: Foo
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# check if smithy4sCodegen works
> compile
$ exists target/scala-2.13/src_managed/main/scala/smithy4s/example/ObjectService.scala
$ exists target/scala-2.13/resource_managed/main/smithy4s.example.ObjectService.json
> checkOpenApi
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private[smithy4s] object JsonConverters {
)

// format: off
type GenTarget = List[os.Path] :*: os.Path :*: os.Path :*: Set[FileType] :*: Boolean:*: Option[Set[String]] :*: Option[Set[String]] :*: List[String] :*: List[String] :*: List[String] :*: List[os.Path] :*: LNil
type GenTarget = List[os.Path] :*: os.Path :*: os.Path :*: Set[FileType] :*: Boolean:*: Option[Set[String]] :*: Option[Set[String]] :*: List[String] :*: List[String] :*: List[String] :*: List[os.Path] :*: Option[os.Path] :*: LNil
// format: on
implicit val codegenArgsIso = LList.iso[CodegenArgs, GenTarget](
{ ca: CodegenArgs =>
Expand All @@ -76,6 +76,7 @@ private[smithy4s] object JsonConverters {
("dependencies", ca.dependencies) :*:
("transformers", ca.transformers) :*:
("localJars", ca.localJars) :*:
("smithyBuild", ca.smithyBuild) :*:
LNil
},
{
Expand All @@ -89,7 +90,8 @@ private[smithy4s] object JsonConverters {
(_, repositories) :*:
(_, dependencies) :*:
(_, transformers) :*:
(_, localJars) :*: LNil =>
(_, localJars) :*:
(_, smithyBuild) :*: LNil =>
CodegenArgs(
specs,
output,
Expand All @@ -101,7 +103,8 @@ private[smithy4s] object JsonConverters {
repositories,
dependencies,
transformers,
localJars
localJars,
smithyBuild
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ object Smithy4sCodegenPlugin extends AutoPlugin {
).mkString(" ")
)

val smithyBuild = taskKey[Option[File]](
"smithy-build.json to use for reading build configuration"
)

val smithy4sWildcardArgument =
taskKey[String](
"String value to use as wildcard argument in types in generated code"
Expand Down Expand Up @@ -230,6 +234,7 @@ object Smithy4sCodegenPlugin extends AutoPlugin {
(config / smithy4sInternalDependenciesAsJars).value ++
fetch(config / smithy4sAllExternalDependencies).value
},
config / smithyBuild := None,
config / smithy4sWildcardArgument := {
// This logic configures the default wildcard argument based on the scala version and scalac options
// In the following scenarios we use "?" instead of "_"
Expand Down Expand Up @@ -423,6 +428,7 @@ object Smithy4sCodegenPlugin extends AutoPlugin {
val skipSet = skipResources

val filePaths = inputFiles.map(_.getAbsolutePath())
val smithyBuildValue = (conf / smithyBuild).value.map(os.Path(_))
val codegenArgs = CodegenArgs(
filePaths.map(os.Path(_)).toList,
output = os.Path(outputPath),
Expand All @@ -434,7 +440,8 @@ object Smithy4sCodegenPlugin extends AutoPlugin {
repositories = res,
dependencies = List.empty,
transformers = transforms,
localJars = localJars
localJars = localJars,
smithyBuild = smithyBuildValue
)

val cached =
Expand Down
3 changes: 2 additions & 1 deletion modules/codegen/src/smithy4s/codegen/CodegenArgs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ final case class CodegenArgs(
repositories: List[String],
dependencies: List[String],
transformers: List[String],
localJars: List[os.Path]
localJars: List[os.Path],
smithyBuild: Option[os.Path]
) {
def skipScala: Boolean = skip(FileType.Scala)
def skipOpenapi: Boolean = skip(FileType.Openapi)
Expand Down
2 changes: 1 addition & 1 deletion modules/codegen/src/smithy4s/codegen/SmithyBuildJson.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private[codegen] object SmithyBuildJson {
repositories: ListSet[String]
): String = {
SmithyBuild.writeJson(
SmithyBuild(
SmithyBuild.Serializable(
version = "1.0",
imports,
SmithyBuildMaven(
Expand Down
17 changes: 14 additions & 3 deletions modules/codegen/src/smithy4s/codegen/internals/CodegenImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ import smithy4s.codegen.transformers._
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.ModelSerializer
import software.amazon.smithy.openapi.OpenApiConfig

import scala.jdk.CollectionConverters._
import software.amazon.smithy.model.transform.ModelTransformer

private[codegen] object CodegenImpl { self =>

def generate(args: CodegenArgs): CodegenResult = {
val smithyBuild = args.smithyBuild
.map(os.read)
.map(SmithyBuild.readJson(_))
val (classloader, model): (ClassLoader, Model) = internals.ModelLoader.load(
args.specs.map(_.toIO).toSet,
args.dependencies,
Expand Down Expand Up @@ -64,12 +68,19 @@ private[codegen] object CodegenImpl { self =>
} else (List.empty, List.empty)

val openApiFiles = if (!args.skipOpenapi) {
alloy.openapi.convert(model, args.allowedNS, classloader).map {
case OpenApiConversionResult(_, serviceId, outputString) =>
val openApiConfig: Unit => OpenApiConfig = _ =>
smithyBuild
.flatMap(_.getPlugin[SmithyBuildPlugin.OpenApi])
.map(_.config)
.getOrElse(new OpenApiConfig())

alloy.openapi
.convertWithConfig(model, args.allowedNS, openApiConfig, classloader)
.map { case OpenApiConversionResult(_, serviceId, outputString) =>
val name = serviceId.getNamespace() + "." + serviceId.getName()
val openapiFile = (args.resourceOutput / (name + ".json"))
CodegenEntry.FromMemory(openapiFile, outputString)
}
}
} else List.empty

val protoFiles = if (!args.skipProto) {
Expand Down
Loading

0 comments on commit 001bb47

Please sign in to comment.