diff --git a/CHANGELOG.md b/CHANGELOG.md index a7917c4ed0..2b5e0db64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ When adding entries, please treat them as if they could end up in a release any Thank you! +# 0.18.36 + +* codegen: Pass the correct ClassLoader to prevent validators/transformers from breaking on externally-defined trait classes in [#1709](https://github.com/disneystreaming/smithy4s/pull/1709) + # 0.18.35 * json, documents: Add support for `@jsonUnknown` in unions (Open Unions) in [#1677](https://github.com/disneystreaming/smithy4s/pull/1677) diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/build.sbt b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/build.sbt new file mode 100644 index 0000000000..11b433fe6d --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/build.sbt @@ -0,0 +1,24 @@ +lazy val transformation = project + .settings( + scalaVersion := "2.12.20", + libraryDependencies ++= Seq( + "software.amazon.smithy" % "smithy-build" % "1.57.1", + "ch.epfl.scala" % "spec-traits" % "2.2.0-M2" + ) + ) + +lazy val root = project + .in(file(".")) + .enablePlugins(Smithy4sCodegenPlugin) + .settings( + scalaVersion := "3.3.6", + libraryDependencies ++= Seq( + "ch.epfl.scala" % "spec-traits" % "2.2.0-M2" % Smithy4s, + "com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion.value + ), + Compile / smithy4sModelTransformers := List( + "my-transformation" + ), + Compile / smithy4sAllowedNamespaces := List("my.input"), + Compile / smithy4sAllDependenciesAsJars += (transformation / Compile / packageBin).value + ) diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/project/plugins.sbt b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/project/plugins.sbt new file mode 100644 index 0000000000..b8589b92c5 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/project/plugins.sbt @@ -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 + ) +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/src/main/scala/Main.scala b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/src/main/scala/Main.scala new file mode 100644 index 0000000000..a124c8c859 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/src/main/scala/Main.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2025 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. + */ + +object Main extends App { + val hints = my.input.MyShape.schema.hints + + require( + hints.has[smithy.api.Documentation], + s"Expected to have the Documentation trait, but it was missing: $hints" + ) + + require( + hints.get[smithy.api.Documentation].get.value == "what's up doc", + s"Documentation trait mismatch: ${hints.get[smithy.api.Documentation].get.value}" + ) + println("all good: " + hints.all) +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/src/main/smithy/input.smithy b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/src/main/smithy/input.smithy new file mode 100644 index 0000000000..4792e57364 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/src/main/smithy/input.smithy @@ -0,0 +1,6 @@ +$version: "2" + +namespace my.input + +@traits#data +document MyShape diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/test b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/test new file mode 100644 index 0000000000..642afb2518 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/test @@ -0,0 +1,4 @@ +# check if the app runs successfully. +# if it doesn't compile, it could be because the transformation wasn't applied. +# if the transformation throws, it's because of a bug like #336. +> run diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/transformation/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/transformation/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer new file mode 100644 index 0000000000..d7a892c243 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/transformation/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -0,0 +1 @@ +MyTransformation diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/transformation/src/main/scala/MyTransformation.scala b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/transformation/src/main/scala/MyTransformation.scala new file mode 100644 index 0000000000..a61a1a0ccb --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/transformation-trait-classes/transformation/src/main/scala/MyTransformation.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2021-2025 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. + */ + +import software.amazon.smithy.build.ProjectionTransformer +import software.amazon.smithy.build.TransformContext +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.transform.ModelTransformer +import java.util.function.BiFunction +import software.amazon.smithy.model.traits.Trait +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.DocumentationTrait +import software.amazon.smithy.model.shapes.ShapeId +import bsp.traits.DataTrait + +class MyTransformation extends ProjectionTransformer { + def getName(): String = "my-transformation" + + // Replace traits#jsonRPC with documentation + def transform(context: TransformContext): Model = { + // this would fail if the class wasn't present on the classpath. + + // external shape - regression test for #336 + context + .getModel() + .expectShape(ShapeId.from("bsp#BuildTargetData")) + .expectTrait(classOf[DataTrait]) + + // local shape + context + .getModel() + .expectShape(ShapeId.from("my.input#MyShape")) + .expectTrait(classOf[DataTrait]) + + ModelTransformer + .create() + .mapTraits( + context.getModel(), + { + case (_, _: DataTrait) => + new DocumentationTrait("what's up doc") + case (_, trt) => trt + }: BiFunction[Shape, Trait, Trait] + ) + } + +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/build.sbt b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/build.sbt new file mode 100644 index 0000000000..6452e9ebad --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/build.sbt @@ -0,0 +1,19 @@ +lazy val externalLibrary = project + .settings( + autoScalaLibrary := false, + crossPaths := false, + libraryDependencies ++= Seq( + "software.amazon.smithy" % "smithy-model" % "1.57.1" + ) + ) + +lazy val root = project + .in(file(".")) + .enablePlugins(Smithy4sCodegenPlugin) + .settings( + scalaVersion := "3.3.6", + libraryDependencies ++= Seq( + "com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion.value + ), + Compile / smithy4sAllDependenciesAsJars += (externalLibrary / Compile / packageBin).value + ) diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/java/ApiVersionTrait.java b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/java/ApiVersionTrait.java new file mode 100644 index 0000000000..9b643b57c9 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/java/ApiVersionTrait.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021-2025 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. + */ + +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.StringTrait; + +public class ApiVersionTrait extends StringTrait { + + public static ShapeId ID = ShapeId.from("my.input#apiVersion"); + + public ApiVersionTrait(String value, SourceLocation sourceLocation) { + super(ID, value, sourceLocation); + } + + public ApiVersionTrait(String value) { + this(value, SourceLocation.NONE); + } + + public static final class Provider extends StringTrait.Provider { + public Provider() { + super(ID, ApiVersionTrait::new); + } + } + +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/java/ApiVersioningValidator.java b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/java/ApiVersioningValidator.java new file mode 100644 index 0000000000..32b40ac6b8 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/java/ApiVersioningValidator.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2025 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. + */ + +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class ApiVersioningValidator extends AbstractValidator { + + @Override + public List validate(Model model) { + model.getShapesWithTrait(ApiVersionTrait.ID).stream().forEach(shape -> { + System.out.println("validating shape: " + shape); + shape.expectTrait(ApiVersionTrait.class); + }); + + return List.of(); + } +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService new file mode 100644 index 0000000000..3bb4b0846e --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -0,0 +1 @@ +ApiVersionTrait$Provider diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator new file mode 100644 index 0000000000..b5b6859191 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -0,0 +1 @@ +ApiVersioningValidator diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/smithy/input.smithy b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/smithy/input.smithy new file mode 100644 index 0000000000..097ccf127d --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/smithy/input.smithy @@ -0,0 +1,9 @@ +$version: "2" + +namespace my.input + +@trait(selector: "service") +string apiVersion + +@apiVersion("v1") +service FooService {} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/smithy/manifest b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 0000000000..b6f5f8d85d --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/externalLibrary/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +input.smithy diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/project/plugins.sbt b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/project/plugins.sbt new file mode 100644 index 0000000000..b8589b92c5 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/project/plugins.sbt @@ -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 + ) +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/test b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/test new file mode 100644 index 0000000000..2a43a4c0a9 --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/validator-trait-classes/test @@ -0,0 +1,3 @@ +# check if the app compiles. +# if the validator throws, it's because of a bug like #336. +> compile diff --git a/modules/codegen-plugin/src/smithy4s/codegen/GenerateSmithyBuild.scala b/modules/codegen-plugin/src/smithy4s/codegen/GenerateSmithyBuild.scala index 3e709800e9..6fc494e4ba 100644 --- a/modules/codegen-plugin/src/smithy4s/codegen/GenerateSmithyBuild.scala +++ b/modules/codegen-plugin/src/smithy4s/codegen/GenerateSmithyBuild.scala @@ -19,9 +19,10 @@ package smithy4s.codegen import sbt.Keys._ import sbt._ -import Smithy4sCodegenPlugin.autoImport._ import scala.collection.immutable.ListSet +import Smithy4sCodegenPlugin.autoImport._ + private final case class SmithyBuildData( sources: ListSet[String], deps: ListSet[String], diff --git a/modules/codegen-plugin/src/smithy4s/codegen/JsonConverters.scala b/modules/codegen-plugin/src/smithy4s/codegen/JsonConverters.scala index 513bed3ecd..4dc1a45559 100644 --- a/modules/codegen-plugin/src/smithy4s/codegen/JsonConverters.scala +++ b/modules/codegen-plugin/src/smithy4s/codegen/JsonConverters.scala @@ -16,13 +16,14 @@ package smithy4s.codegen -import sjsonnew._ -import BasicJsonProtocol._ -import sbt.FileInfo -import sbt.HashFileInfo import cats.data.Validated.Invalid import cats.data.Validated.Valid +import sbt.FileInfo +import sbt.HashFileInfo import sbt.io.Hash +import sjsonnew._ + +import BasicJsonProtocol._ // Json codecs used by SBT's caching constructs private[smithy4s] object JsonConverters { diff --git a/modules/codegen/src/smithy4s/codegen/internals/ModelLoader.scala b/modules/codegen/src/smithy4s/codegen/internals/ModelLoader.scala index 269efea200..43d10f4957 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/ModelLoader.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/ModelLoader.scala @@ -74,9 +74,14 @@ private[codegen] object ModelLoader { } } + val validatorClassLoader = locally { + val jarUrls = deps.map(_.toURI().toURL()).toArray + new URLClassLoader(jarUrls, currentClassLoader) + } + // Loading the upstream model val upstreamModel = Model - .assembler() + .assembler(validatorClassLoader) // disabling cache to support snapshot-driven experimentation .putProperty(ModelAssembler.DISABLE_JAR_CACHE, true) .addClasspathModels(currentClassLoader, discoverModels) @@ -95,11 +100,6 @@ private[codegen] object ModelLoader { case _ => () } - val validatorClassLoader = locally { - val jarUrls = deps.map(_.toURI().toURL()).toArray - new URLClassLoader(jarUrls, currentClassLoader) - } - val preTransformationModel = Model .assembler(validatorClassLoader)