diff --git a/build.sbt b/build.sbt index 7ca7ffaaf..d0a43ad19 100644 --- a/build.sbt +++ b/build.sbt @@ -299,6 +299,9 @@ lazy val `aws-http4s` = projectMatrix bootstrapped % "test->compile" ) .settings( + // TODO: Remove once https://github.com/disneystreaming/alloy/pull/135 is + // merged and released. + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), libraryDependencies ++= { Seq( Dependencies.Fs2.io.value, @@ -314,6 +317,9 @@ lazy val `aws-http4s` = projectMatrix "-Wconf:msg=class RestXml in package (aws\\.)?protocols is deprecated:silent", "-Wconf:msg=value noErrorWrapping in class RestXml is deprecated:silent" ), + Test / complianceTestRepositories := Seq( + "https://s01.oss.sonatype.org/content/repositories/snapshots" + ), Test / complianceTestDependencies := Seq( Dependencies.Smithy.`aws-protocol-tests` ), @@ -365,6 +371,9 @@ lazy val codegen = projectMatrix "alloyVersion" -> Dependencies.Alloy.alloyVersion ), buildInfoPackage := "smithy4s.codegen", + // TODO: Remove once https://github.com/disneystreaming/alloy/pull/135 is + // merged and released. + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), libraryDependencies ++= Seq( Dependencies.Cats.core.value, Dependencies.Smithy.model, @@ -561,6 +570,9 @@ lazy val dynamic = projectMatrix bootstrapped % "test->test;test->compile" ) .settings( + // TODO: Remove once https://github.com/disneystreaming/alloy/pull/135 is + // merged and released. + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), libraryDependencies ++= munitDeps.value ++ Seq( Dependencies.collectionsCompat.value, Dependencies.Cats.core.value, @@ -691,6 +703,9 @@ lazy val http4s = projectMatrix ) .settings( isMimaEnabled := true, + // TODO: Remove once https://github.com/disneystreaming/alloy/pull/135 is + // merged and released. + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), libraryDependencies ++= { Seq( Dependencies.Http4s.core.value, @@ -708,6 +723,9 @@ lazy val http4s = projectMatrix Test / allowedNamespaces := Seq( "smithy4s.example.guides.auth" ), + Test / complianceTestRepositories := Seq( + "https://s01.oss.sonatype.org/content/repositories/snapshots" + ), Test / complianceTestDependencies := Seq( Dependencies.Alloy.`protocol-tests` ), @@ -798,6 +816,9 @@ lazy val transformers = projectMatrix .in(file("modules/transformers")) .settings(Smithy4sBuildPlugin.doNotPublishArtifact) .settings( + // TODO: Remove once https://github.com/disneystreaming/alloy/pull/135 is + // merged and released. + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), libraryDependencies ++= Seq( Dependencies.Smithy.model, Dependencies.Smithy.build, @@ -964,7 +985,10 @@ def genSmithy(config: Configuration) = Def.settings( def genSmithyScala(config: Configuration) = genSmithyImpl(config).map(_._1) def genSmithyResources(config: Configuration) = genSmithyImpl(config).map(_._2) -// SBT setting to specify artifacts to be included in the Smithy model for compliance testing +// SBT settings to specify repositories and their artifacts to be included in +// the Smithy model for compliance testing +val complianceTestRepositories = + SettingKey[Seq[String]]("complianceTestRepositories") val complianceTestDependencies = SettingKey[Seq[ModuleID]]("complianceTestDependencies") @@ -976,6 +1000,8 @@ def dumpModel(config: Configuration): Def.Initialize[Task[Seq[File]]] = Smithy4sBuildPlugin.Scala213 ) / Compile / fullClasspath).value .map(_.data) + val repos = + (config / complianceTestRepositories).?.value.getOrElse(Seq.empty) val transforms = (config / smithy4sModelTransformers).value lazy val modelTransformersCp = (transformers.jvm( Smithy4sBuildPlugin.Scala213 @@ -1023,8 +1049,10 @@ def dumpModel(config: Configuration): Def.Initialize[Task[Seq[File]]] = val s = (config / streams).value val args = - if (transforms.isEmpty) List.empty - else List("--transformers", transforms.mkString(",")) + (if (repos.isEmpty) List.empty + else List("--repositories", repos.mkString(","))) ++ + (if (transforms.isEmpty) List.empty + else List("--transformers", transforms.mkString(","))) val cached = Tracked.inputChanged[List[String], Seq[File]]( s.cacheStoreFactory.make("input") @@ -1077,6 +1105,8 @@ def genSmithyImpl(config: Configuration) = Def.task { .getAbsolutePath() val allowedNS = (config / allowedNamespaces).?.value.filterNot(_.isEmpty) val skip = (config / smithy4sSkip).?.value.getOrElse(Seq.empty) + val smithy4sRepos = + (config / smithy4sRepositories).?.value.getOrElse(Seq.empty) val smithy4sDeps = (config / smithy4sDependencies).?.value.getOrElse(Seq.empty).map { moduleId => @@ -1154,6 +1184,10 @@ def genSmithyImpl(config: Configuration) = Def.task { List("--allowed-ns", allowedNS.get.mkString(",")) else Nil val skipOpt = skip.flatMap(s => List("--skip", s)) + val respositoriesOpt = + if (smithy4sRepos.nonEmpty) + List("--repositories", smithy4sRepos.mkString(",")) + else Nil val dependenciesOpt = if (smithy4sDeps.nonEmpty) List("--dependencies", smithy4sDeps.mkString(",")) @@ -1163,6 +1197,7 @@ def genSmithyImpl(config: Configuration) = Def.task { allowedNsOpt ++ inputs ++ skipOpt ++ + respositoriesOpt ++ dependenciesOpt val cp = codegenCp diff --git a/modules/bootstrapped/src/generated/com/amazonaws/dynamodb/Endpoint.scala b/modules/bootstrapped/src/generated/com/amazonaws/dynamodb/Endpoint.scala index 61b6c6461..48d291c6f 100644 --- a/modules/bootstrapped/src/generated/com/amazonaws/dynamodb/Endpoint.scala +++ b/modules/bootstrapped/src/generated/com/amazonaws/dynamodb/Endpoint.scala @@ -28,6 +28,6 @@ object Endpoint extends ShapeTag.Companion[Endpoint] { implicit val schema: Schema[Endpoint] = struct( string.required[Endpoint]("Address", _.address).addHints(smithy.api.Documentation("

IP address of the endpoint.

")), - long.required[Endpoint]("CachePeriodInMinutes", _.cachePeriodInMinutes).addHints(smithy.api.Default(smithy4s.Document.fromDouble(0.0d)), smithy.api.Documentation("

Endpoint cache time to live (TTL) value.

")), + long.required[Endpoint]("CachePeriodInMinutes", _.cachePeriodInMinutes).addHints(smithy.api.Documentation("

Endpoint cache time to live (TTL) value.

"), smithy.api.Default(smithy4s.Document.fromDouble(0.0d))), )(make).withId(id).addHints(hints) } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala new file mode 100644 index 000000000..dccf8243d --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala @@ -0,0 +1,27 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.document +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class DefaultRequiredUnknownFieldRetentionExample(retainedUnknownFields: Document = smithy4s.Document.nullDoc, foo: Option[String] = None, bar: Option[String] = None) + +object DefaultRequiredUnknownFieldRetentionExample extends ShapeTag.Companion[DefaultRequiredUnknownFieldRetentionExample] { + val id: ShapeId = ShapeId("smithy4s.example", "DefaultRequiredUnknownFieldRetentionExample") + + val hints: Hints = Hints.empty + + // constructor using the original order from the spec + private def make(foo: Option[String], bar: Option[String], retainedUnknownFields: Document): DefaultRequiredUnknownFieldRetentionExample = DefaultRequiredUnknownFieldRetentionExample(retainedUnknownFields, foo, bar) + + implicit val schema: Schema[DefaultRequiredUnknownFieldRetentionExample] = struct( + string.optional[DefaultRequiredUnknownFieldRetentionExample]("foo", _.foo), + string.optional[DefaultRequiredUnknownFieldRetentionExample]("bar", _.bar), + document.required[DefaultRequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(smithy.api.Default(smithy4s.Document.nullDoc), alloy.UnknownJsonFieldRetention(), alloy.UnknownDocumentFieldRetention()), + )(make).withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala new file mode 100644 index 000000000..1f4561e15 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala @@ -0,0 +1,27 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.document +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class DefaultUnknownFieldRetentionExample(retainedUnknownFields: Document = smithy4s.Document.nullDoc, foo: Option[String] = None, bar: Option[String] = None) + +object DefaultUnknownFieldRetentionExample extends ShapeTag.Companion[DefaultUnknownFieldRetentionExample] { + val id: ShapeId = ShapeId("smithy4s.example", "DefaultUnknownFieldRetentionExample") + + val hints: Hints = Hints.empty + + // constructor using the original order from the spec + private def make(foo: Option[String], bar: Option[String], retainedUnknownFields: Document): DefaultUnknownFieldRetentionExample = DefaultUnknownFieldRetentionExample(retainedUnknownFields, foo, bar) + + implicit val schema: Schema[DefaultUnknownFieldRetentionExample] = struct( + string.optional[DefaultUnknownFieldRetentionExample]("foo", _.foo), + string.optional[DefaultUnknownFieldRetentionExample]("bar", _.bar), + document.field[DefaultUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc), alloy.UnknownJsonFieldRetention()), + )(make).withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/Integers.scala b/modules/bootstrapped/src/generated/smithy4s/example/Integers.scala index e640d489e..4f6ca8d16 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/Integers.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/Integers.scala @@ -21,9 +21,9 @@ object Integers extends ShapeTag.Companion[Integers] { implicit val schema: Schema[Integers] = struct( int.required[Integers]("int", _.int).addHints(alloy.proto.ProtoIndex(1)), - int.required[Integers]("sint", _.sint).addHints(alloy.proto.ProtoNumType.SIGNED.widen, alloy.proto.ProtoIndex(2)), - int.required[Integers]("uint", _.uint).addHints(alloy.proto.ProtoNumType.UNSIGNED.widen, alloy.proto.ProtoIndex(3)), - int.required[Integers]("fixedUint", _.fixedUint).addHints(alloy.proto.ProtoNumType.FIXED.widen, alloy.proto.ProtoIndex(4)), - int.required[Integers]("fixedSint", _.fixedSint).addHints(alloy.proto.ProtoNumType.FIXED_SIGNED.widen, alloy.proto.ProtoIndex(5)), + int.required[Integers]("sint", _.sint).addHints(alloy.proto.ProtoIndex(2), alloy.proto.ProtoNumType.SIGNED.widen), + int.required[Integers]("uint", _.uint).addHints(alloy.proto.ProtoIndex(3), alloy.proto.ProtoNumType.UNSIGNED.widen), + int.required[Integers]("fixedUint", _.fixedUint).addHints(alloy.proto.ProtoIndex(4), alloy.proto.ProtoNumType.FIXED.widen), + int.required[Integers]("fixedSint", _.fixedSint).addHints(alloy.proto.ProtoIndex(5), alloy.proto.ProtoNumType.FIXED_SIGNED.widen), )(make).withId(id).addHints(hints) } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/Longs.scala b/modules/bootstrapped/src/generated/smithy4s/example/Longs.scala index 4d193af2e..528fa97e9 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/Longs.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/Longs.scala @@ -21,9 +21,9 @@ object Longs extends ShapeTag.Companion[Longs] { implicit val schema: Schema[Longs] = struct( long.required[Longs]("long", _.long).addHints(alloy.proto.ProtoIndex(1)), - long.required[Longs]("slong", _.slong).addHints(alloy.proto.ProtoNumType.SIGNED.widen, alloy.proto.ProtoIndex(2)), - long.required[Longs]("ulong", _.ulong).addHints(alloy.proto.ProtoNumType.UNSIGNED.widen, alloy.proto.ProtoIndex(3)), - long.required[Longs]("fixedLong", _.fixedLong).addHints(alloy.proto.ProtoNumType.FIXED.widen, alloy.proto.ProtoIndex(4)), - long.required[Longs]("fixedSlong", _.fixedSlong).addHints(alloy.proto.ProtoNumType.FIXED_SIGNED.widen, alloy.proto.ProtoIndex(5)), + long.required[Longs]("slong", _.slong).addHints(alloy.proto.ProtoIndex(2), alloy.proto.ProtoNumType.SIGNED.widen), + long.required[Longs]("ulong", _.ulong).addHints(alloy.proto.ProtoIndex(3), alloy.proto.ProtoNumType.UNSIGNED.widen), + long.required[Longs]("fixedLong", _.fixedLong).addHints(alloy.proto.ProtoIndex(4), alloy.proto.ProtoNumType.FIXED.widen), + long.required[Longs]("fixedSlong", _.fixedSlong).addHints(alloy.proto.ProtoIndex(5), alloy.proto.ProtoNumType.FIXED_SIGNED.widen), )(make).withId(id).addHints(hints) } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala b/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala index 6a6f0b704..feabab44f 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala @@ -22,7 +22,7 @@ object OperationInput extends ShapeTag.Companion[OperationInput] { implicit val schema: Schema[OperationInput] = struct( string.optional[OperationInput]("optional", _.optional), string.field[OperationInput]("optionalWithDefault", _.optionalWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-default"))), - string.required[OperationInput]("requiredLabel", _.requiredLabel).addHints(smithy.api.Default(smithy4s.Document.fromString("required-label-with-default")), smithy.api.HttpLabel()), + string.required[OperationInput]("requiredLabel", _.requiredLabel).addHints(smithy.api.HttpLabel(), smithy.api.Default(smithy4s.Document.fromString("required-label-with-default"))), string.required[OperationInput]("requiredWithDefault", _.requiredWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-default"))), string.optional[OperationInput]("optionalHeader", _.optionalHeader).addHints(smithy.api.HttpHeader("optional-header")), string.field[OperationInput]("optionalHeaderWithDefault", _.optionalHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-header-with-default")), smithy.api.HttpHeader("optional-header-with-default")), diff --git a/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala new file mode 100644 index 000000000..e091a84bd --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala @@ -0,0 +1,27 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.document +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class RequiredUnknownFieldRetentionExample(retainedUnknownFields: Document, foo: Option[String] = None, bar: Option[String] = None) + +object RequiredUnknownFieldRetentionExample extends ShapeTag.Companion[RequiredUnknownFieldRetentionExample] { + val id: ShapeId = ShapeId("smithy4s.example", "RequiredUnknownFieldRetentionExample") + + val hints: Hints = Hints.empty + + // constructor using the original order from the spec + private def make(foo: Option[String], bar: Option[String], retainedUnknownFields: Document): RequiredUnknownFieldRetentionExample = RequiredUnknownFieldRetentionExample(retainedUnknownFields, foo, bar) + + implicit val schema: Schema[RequiredUnknownFieldRetentionExample] = struct( + string.optional[RequiredUnknownFieldRetentionExample]("foo", _.foo), + string.optional[RequiredUnknownFieldRetentionExample]("bar", _.bar), + document.required[RequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), alloy.UnknownJsonFieldRetention()), + )(make).withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala new file mode 100644 index 000000000..d03763136 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala @@ -0,0 +1,27 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.document +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class UnknownFieldRetentionExample(foo: Option[String] = None, bar: Option[String] = None, retainedUnknownFields: Option[Document] = None) + +object UnknownFieldRetentionExample extends ShapeTag.Companion[UnknownFieldRetentionExample] { + val id: ShapeId = ShapeId("smithy4s.example", "UnknownFieldRetentionExample") + + val hints: Hints = Hints.empty + + // constructor using the original order from the spec + private def make(foo: Option[String], bar: Option[String], retainedUnknownFields: Option[Document]): UnknownFieldRetentionExample = UnknownFieldRetentionExample(foo, bar, retainedUnknownFields) + + implicit val schema: Schema[UnknownFieldRetentionExample] = struct( + string.optional[UnknownFieldRetentionExample]("foo", _.foo), + string.optional[UnknownFieldRetentionExample]("bar", _.bar), + document.optional[UnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), alloy.UnknownJsonFieldRetention()), + )(make).withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/guides/auth/HelloWorldAuthService.scala b/modules/bootstrapped/src/generated/smithy4s/example/guides/auth/HelloWorldAuthService.scala index a678023e3..4292a0fdf 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/guides/auth/HelloWorldAuthService.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/guides/auth/HelloWorldAuthService.scala @@ -159,7 +159,7 @@ object HelloWorldAuthServiceOperation { .withInput(unit) .withError(HealthCheckError.errorSchema) .withOutput(HealthCheckOutput.schema) - .withHints(smithy.api.Auth(Set()), smithy.api.Http(method = smithy.api.NonEmptyString("GET"), uri = smithy.api.NonEmptyString("/health"), code = 200), smithy.api.Readonly()) + .withHints(smithy.api.Http(method = smithy.api.NonEmptyString("GET"), uri = smithy.api.NonEmptyString("/health"), code = 200), smithy.api.Readonly(), smithy.api.Auth(Set())) def wrap(input: Unit): HealthCheck = HealthCheck() } sealed trait HealthCheckError extends scala.Product with scala.Serializable { self => diff --git a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala index 17fdcaddb..95128540e 100644 --- a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala @@ -18,10 +18,14 @@ package smithy4s import smithy.api.JsonName import smithy.api.Default -import smithy4s.example.IntList import alloy.Discriminated import munit._ +import smithy4s.example.DefaultRequiredUnknownFieldRetentionExample +import smithy4s.example.DefaultUnknownFieldRetentionExample +import smithy4s.example.IntList import smithy4s.example.OperationOutput +import smithy4s.example.RequiredUnknownFieldRetentionExample +import smithy4s.example.UnknownFieldRetentionExample class DocumentSpec() extends FunSuite { @@ -499,6 +503,7 @@ class DocumentSpec() extends FunSuite { ) } + test( "document encoder - default values overrides + explicit defaults encoding = false" ) { @@ -572,4 +577,134 @@ class DocumentSpec() extends FunSuite { assertEquals(niceSyntaxDocument, expectedDocument) } + test( + "document codec - unknown field retention + explicit defaults encoding = false" + ) { + val unknownFieldRetentionExample = UnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Some( + Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + ) + + val document = Document.Encoder + .withExplicitDefaultsEncoding(false) + .fromSchema(UnknownFieldRetentionExample.schema) + .encode(unknownFieldRetentionExample) + import Document._ + val expectedDocument = Document.obj( + "foo" -> Document.fromString("foo"), + "bar" -> Document.fromString("bar"), + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + + val roundTripped = Document.decode[UnknownFieldRetentionExample](document) + + expect.same(document, expectedDocument) + expect.same(roundTripped, Right(unknownFieldRetentionExample)) + } + test( + "document codec - unknown field retention + explicit defaults encoding = true" + ) { + val defaultUnknownFieldRetentionExample = + DefaultUnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + + val document = Document.Encoder + .withExplicitDefaultsEncoding(true) + .fromSchema(DefaultUnknownFieldRetentionExample.schema) + .encode(defaultUnknownFieldRetentionExample) + import Document._ + val expectedDocument = Document.obj( + "foo" -> Document.fromString("foo"), + "bar" -> Document.fromString("bar"), + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + + val roundTripped = + Document.decode[DefaultUnknownFieldRetentionExample](document) + + expect.same(document, expectedDocument) + expect.same(roundTripped, Right(defaultUnknownFieldRetentionExample)) + } + + test( + "document codec - required unknown field retention + explicit defaults encoding = false" + ) { + val requiredUnknownFieldRetentionExample = + RequiredUnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + + val document = Document.Encoder + .withExplicitDefaultsEncoding(false) + .fromSchema(RequiredUnknownFieldRetentionExample.schema) + .encode(requiredUnknownFieldRetentionExample) + import Document._ + val expectedDocument = Document.obj( + "foo" -> Document.fromString("foo"), + "bar" -> Document.fromString("bar"), + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + + val roundTripped = + Document.decode[RequiredUnknownFieldRetentionExample](document) + + expect.same(document, expectedDocument) + expect.same(roundTripped, Right(requiredUnknownFieldRetentionExample)) + } + + test( + "document codec - required unknown field retention + explicit defaults encoding = true" + ) { + val defaultRequiredUnknownFieldRetentionExample = + DefaultRequiredUnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + + val document = Document.Encoder + .withExplicitDefaultsEncoding(true) + .fromSchema(DefaultRequiredUnknownFieldRetentionExample.schema) + .encode(defaultRequiredUnknownFieldRetentionExample) + import Document._ + val expectedDocument = Document.obj( + "foo" -> Document.fromString("foo"), + "bar" -> Document.fromString("bar"), + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + + val roundTripped = + Document.decode[DefaultRequiredUnknownFieldRetentionExample](document) + + expect.same(document, expectedDocument) + expect.same( + roundTripped, + Right(defaultRequiredUnknownFieldRetentionExample) + ) + } + } diff --git a/modules/codegen-cli/src/smithy4s/codegen/cli/Main.scala b/modules/codegen-cli/src/smithy4s/codegen/cli/Main.scala index 08cb64e10..791c5af03 100644 --- a/modules/codegen-cli/src/smithy4s/codegen/cli/Main.scala +++ b/modules/codegen-cli/src/smithy4s/codegen/cli/Main.scala @@ -47,12 +47,11 @@ object Main { if (res.isEmpty) { // Printing to stderr because we print generated files path to stdout Console.err.println( - List( - "Nothing was generated. Make sure your targetting Smithy files or folders", - "that include Smithy definitions. Otherwise, you can also use", - "--dependencies to pull external JARs or use --local-jars to use", - "JARs located on your file system." - ).mkString(" ") + "Nothing was generated. Make sure you're targetting Smithy " + + "files or folders that include Smithy definitions. " + + "Alternatively, you can use --dependencies to pull external " + + "JARs or use --local-jars to use JARs located on your file " + + "system." ) } res.foreach(out.println) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index abaa7e9b7..7377955de 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -19,6 +19,7 @@ package internals import alloy.Discriminated import alloy.Nullable +import alloy.UnknownDocumentFieldRetention import smithy.api.JsonName import smithy.api.TimestampFormat import smithy.api.TimestampFormat.DATE_TIME @@ -313,60 +314,65 @@ class DocumentDecoderSchemaVisitor( fields: Vector[Field[S, _]], make: IndexedSeq[Any] => S ): DocumentDecoder[S] = { - def jsonLabel[A](field: Field[S, A]): String = + def isForUnknownFieldRetention(field: Field[S, _]): Boolean = + field.hints.has(UnknownDocumentFieldRetention) + def jsonLabelOrLabel(field: Field[S, _]): String = field.hints.get(JsonName).map(_.value).getOrElse(field.label) - - def fieldDecoder[A]( - field: Field[S, A] - ): ( + val knownFieldLabels = + fields.filterNot(isForUnknownFieldRetention).map(jsonLabelOrLabel) + def fieldDecoder[A](field: Field[S, A]): ( List[PayloadPath.Segment], Any => Unit, Map[String, Document] - ) => Unit = { - val jLabel = jsonLabel(field) - - field.getDefaultValue match { - case Some(defaultValue) => - ( - pp: List[PayloadPath.Segment], - buffer: Any => Unit, - fields: Map[String, Document] - ) => - val path = PayloadPath.Segment(jLabel) :: pp - fields - .get(jLabel) match { - case Some(document) => - buffer(apply(field.schema)(path, document)) - case None => - buffer(defaultValue) - } - case None => - ( - pp: List[PayloadPath.Segment], - buffer: Any => Unit, - fields: Map[String, Document] - ) => - val path = PayloadPath.Segment(jLabel) :: pp - fields - .get(jLabel) match { - case Some(document) => - buffer(apply(field.schema)(path, document)) - case None => + ) => Unit = + if (isForUnknownFieldRetention(field)) { + val unknownFieldsDecoder = Document.Decoder.fromSchema(field.schema) + (pp, buffer, fields) => + val unknownFields = fields -- knownFieldLabels + buffer( + unknownFieldsDecoder + .decode(Document.DObject(unknownFields)) + .getOrElse( throw new PayloadError( - PayloadPath(path.reverse), - "", - "Required field not found" + PayloadPath(pp.reverse), + "Json document", + "Expected Json Shape: Object" ) - } - } - } + ) + ) + } else { + val jsonLabel = jsonLabelOrLabel(field) + field.getDefaultValue match { + case Some(defaultValue) => + (pp, buffer, fields) => + val path = PayloadPath.Segment(jsonLabel) :: pp + fields.get(jsonLabel) match { + case Some(document) => + buffer(apply(field.schema)(path, document)) + case None => + buffer(defaultValue) + } + case None => + (pp, buffer, fields) => + val path = PayloadPath.Segment(jsonLabel) :: pp + fields.get(jsonLabel) match { + case Some(document) => + buffer(apply(field.schema)(path, document)) + case None => + throw new PayloadError( + PayloadPath(path.reverse), + "", + "Required field not found" + ) + } + } + } val fieldDecoders = fields.map(field => fieldDecoder(field)) - DocumentDecoder.instance("Structure", "Object") { case (pp, DObject(value)) => val buffer = Vector.newBuilder[Any] - fieldDecoders.foreach(fd => fd(pp, buffer.+=(_), value)) + fieldDecoders.foreach(fd => fd(pp, buffer += (_), value)) make(buffer.result()) } } @@ -451,16 +457,16 @@ class DocumentDecoderSchemaVisitor( alternatives: Vector[Alt[U, _]], dispatch: Alt.Dispatcher[U] ): DocumentDecoder[U] = { - def jsonLabel[A](alt: Alt[U, A]): String = + def jsonLabelOrLabel[A](alt: Alt[U, A]): String = alt.schema.hints.get(JsonName).map(_.value).getOrElse(alt.label) val decoders: DecoderMap[U] = alternatives.map { case alt @ Alt(_, instance, inject, _) => - val label = jsonLabel(alt) + val jsonLabel = jsonLabelOrLabel(alt) val encoder = { (pp: List[PayloadPath.Segment], doc: Document) => - inject(apply(instance)(label :: pp, doc)) + inject(apply(instance)(jsonLabel :: pp, doc)) } - jsonLabel(alt) -> encoder + jsonLabelOrLabel(alt) -> encoder }.toMap hints match { diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index 37204c562..4f3674953 100644 --- a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala @@ -23,6 +23,8 @@ import smithy.api.TimestampFormat.DATE_TIME import smithy.api.TimestampFormat.EPOCH_SECONDS import smithy.api.TimestampFormat.HTTP_DATE import alloy.Discriminated +import alloy.UnknownDocumentFieldRetention +import alloy.Untagged import smithy4s.capability.EncoderK import smithy4s.schema._ @@ -43,7 +45,6 @@ import smithy4s.schema.Primitive.PUUID import smithy4s.schema.Primitive.PDouble import smithy4s.schema.Primitive.PLong import smithy4s.schema.Primitive.PString -import alloy.Untagged trait DocumentEncoder[A] { self => @@ -195,26 +196,33 @@ class DocumentEncoderSchemaVisitor( } def fieldEncoder[A]( field: Field[S, A] - ): (S, Builder[(String, Document), Map[String, Document]]) => Unit = { - val encoder = apply(field.schema) - val jsonLabel = field.hints - .get(JsonName) - .map(_.value) - .getOrElse(field.label) - (s, builder) => - if (explicitDefaultsEncoding) { - builder.+=(jsonLabel -> encoder.apply(field.get(s))) - } else - field.getUnlessDefault(s).foreach { value => - builder.+=(jsonLabel -> encoder.apply(value)) + ): (S, Builder[(String, Document), Map[String, Document]]) => Unit = + if (field.hints.has(UnknownDocumentFieldRetention)) { + val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) + (s, builder) => + unknownFieldsEncoder.encode(field.get(s)) match { + case Document.DObject(values) => builder ++= values + case _ => () } - } - - val encoders = fields.map(field => fieldEncoder(field)) + } else { + val encoder = apply(field.schema) + val jsonLabel = field.hints + .get(JsonName) + .map(_.value) + .getOrElse(field.label) + if (explicitDefaultsEncoding) { (s, builder) => + builder.+=(jsonLabel -> encoder.apply(field.get(s))) + } else { (s, builder) => + field + .getUnlessDefault(s) + .foreach(value => builder += jsonLabel -> encoder.apply(value)) + } + } + val fieldEncoders = fields.map(field => fieldEncoder(field)) new DocumentEncoder[S] { def apply(s: S): Document = { val builder = Map.newBuilder[String, Document] - encoders.foreach(_(s, builder)) + fieldEncoders.foreach(_(s, builder)) DObject(builder.result() ++ discriminator) } } diff --git a/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala b/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala index abd63fdd4..d4a218e16 100644 --- a/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala +++ b/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala @@ -88,7 +88,8 @@ object JsoniterCodecCompiler { DiscriminatedUnionMember, Default, Required, - Nullable + Nullable, + UnknownJsonFieldRetention ) } diff --git a/modules/json/src/smithy4s/json/internals/JCodec.scala b/modules/json/src/smithy4s/json/internals/JCodec.scala index 9e611beb8..7e3667567 100644 --- a/modules/json/src/smithy4s/json/internals/JCodec.scala +++ b/modules/json/src/smithy4s/json/internals/JCodec.scala @@ -20,14 +20,6 @@ package internals import com.github.plokhotnyuk.jsoniter_scala.core._ -/** - * Construct that expresses the ability to decode an http message, - * the metadata of which will have already been decoded and staged - * in a Map[String, Any] indexed by field. - * - * On the encoding side, the fields that should be stored in metadata - * are eluded. - */ private[internals] trait JCodec[A] extends JsonCodec[A] { self => @@ -35,14 +27,6 @@ private[internals] trait JCodec[A] extends JsonCodec[A] { def expecting: String - /** - * States whether this codec expects data - * from the body of an http request (as opposed to - * from headers, query params, etc). Used to prevent - * parsing altogether when not required. - */ - def expectBody: Boolean = true - def decodeValue(cursor: Cursor, in: JsonReader): A override final def decodeValue(in: JsonReader, default: A): A = diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index e4528c37a..773aa9989 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -27,6 +27,7 @@ import smithy.api.JsonName import smithy.api.TimestampFormat import alloy.Discriminated import alloy.Nullable +import alloy.UnknownJsonFieldRetention import alloy.Untagged import smithy4s.internals.DiscriminatedUnionMember import smithy4s.schema._ @@ -36,8 +37,8 @@ import smithy4s.Timestamp import scala.collection.compat.immutable.ArraySeq import scala.collection.immutable.VectorBuilder import scala.collection.mutable.ListBuffer -import scala.collection.mutable.{Map => MMap} import scala.collection.immutable.ListMap +import scala.jdk.CollectionConverters._ private[smithy4s] class SchemaVisitorJCodec( maxArity: Int, @@ -47,7 +48,6 @@ private[smithy4s] class SchemaVisitorJCodec( preserveMapOrder: Boolean, val cache: CompilationCache[JCodec] ) extends SchemaVisitor.Cached[JCodec] { self => - private val emptyMetadata: MMap[String, Any] = MMap.empty object PrimitiveJCodecs { val boolean: JCodec[Boolean] = @@ -490,7 +490,7 @@ private[smithy4s] class SchemaVisitorJCodec( } private def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -578,7 +578,7 @@ private[smithy4s] class SchemaVisitorJCodec( out.encodeError("Cannot use vectors as keys") private[this] def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -626,7 +626,7 @@ private[smithy4s] class SchemaVisitorJCodec( out.encodeError("Cannot use vectors as keys") private[this] def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -687,7 +687,7 @@ private[smithy4s] class SchemaVisitorJCodec( out.encodeError("Cannot use vectors as keys") private[this] def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -734,7 +734,7 @@ private[smithy4s] class SchemaVisitorJCodec( out.encodeError("Cannot use vectors as keys") private[this] def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -793,7 +793,7 @@ private[smithy4s] class SchemaVisitorJCodec( out.encodeError("Cannot use maps as keys") private[this] def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -864,7 +864,7 @@ private[smithy4s] class SchemaVisitorJCodec( out.encodeError("Cannot use maps as keys") private def maxArityError(cursor: Cursor): Nothing = - throw cursor.payloadError( + cursor.payloadError( this, s"Input $expecting exceeded max arity of $maxArity" ) @@ -937,7 +937,7 @@ private[smithy4s] class SchemaVisitorJCodec( override def canBeKey: Boolean = false - def jsonLabel[A](alt: Alt[U, A]): String = + def jsonLabelOrLabel[A](alt: Alt[U, A]): String = alt.hints.get(JsonName) match { case None => alt.label case Some(x) => x.value @@ -951,7 +951,7 @@ private[smithy4s] class SchemaVisitorJCodec( alt.inject(cursor.decode(codec, reader)) } - alternatives.foreach(alt => put(jsonLabel(alt), handler(alt))) + alternatives.foreach(alt => put(jsonLabelOrLabel(alt), handler(alt))) } def decodeValue(cursor: Cursor, in: JsonReader): U = @@ -1068,7 +1068,7 @@ private[smithy4s] class SchemaVisitorJCodec( override def canBeKey: Boolean = false - def jsonLabel[A](alt: Alt[U, A]): String = + def jsonLabelOrLabel[A](alt: Alt[U, A]): String = alt.hints.get(JsonName) match { case None => alt.label case Some(x) => x.value @@ -1084,7 +1084,7 @@ private[smithy4s] class SchemaVisitorJCodec( alt.inject(cursor.decode(codec, reader)) } - alternatives.foreach(alt => put(jsonLabel(alt), handler(alt))) + alternatives.foreach(alt => put(jsonLabelOrLabel(alt), handler(alt))) } def decodeValue(cursor: Cursor, in: JsonReader): U = @@ -1275,7 +1275,11 @@ private[smithy4s] class SchemaVisitorJCodec( } } - private def jsonLabel[A, Z](field: Field[Z, A]): String = + private def isForUnknownFieldRetention[Z](field: Field[Z, _]): Boolean = + field.hints + .has(UnknownJsonFieldRetention) + + private def jsonLabelOrLabel[Z](field: Field[Z, _]): String = field.hints.get(JsonName) match { case None => field.label case Some(x) => x.value @@ -1288,8 +1292,8 @@ private[smithy4s] class SchemaVisitorJCodec( ): Handler = { val codec = apply(field.schema) val label = field.label - (cursor, in, mmap) => - val _ = mmap.put( + (cursor, in, knownFields) => + val _ = knownFields.put( label, { cursor.push(label) val result = cursor.decode(codec, in) @@ -1301,33 +1305,47 @@ private[smithy4s] class SchemaVisitorJCodec( private def fieldEncoder[Z, A]( field: Field[Z, A] - ): (Z, JsonWriter) => Unit = { - val codec = apply(field.schema) - val jLabel = jsonLabel(field) - val writeLabel: JsonWriter => Unit = - if (jLabel.forall(JsonWriter.isNonEscapedAscii)) { - _.writeNonEscapedAsciiKey(jLabel) - } else _.writeKey(jLabel) - - if (explicitDefaultsEncoding) { (z: Z, out: JsonWriter) => - writeLabel(out) - codec.encodeValue(field.get(z), out) - } else { (z: Z, out: JsonWriter) => - field.foreachUnlessDefault(z) { (a: A) => + ): (Z, JsonWriter) => Unit = + if (isForUnknownFieldRetention(field)) { + val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) + (z, out) => + unknownFieldsEncoder.encode(field.get(z)) match { + case Document.DObject(values) => + values.foreach { case (key, value) => + out.writeKey(key) + documentJCodec.encodeValue(value, out) + } + + case _ => + () + } + } else { + val codec = apply(field.schema) + val jsonLabel = jsonLabelOrLabel(field) + val writeLabel: JsonWriter => Unit = + if (jsonLabel.forall(JsonWriter.isNonEscapedAscii)) { + _.writeNonEscapedAsciiKey(jsonLabel) + } else _.writeKey(jsonLabel) + + if (explicitDefaultsEncoding) { (z, out) => writeLabel(out) - codec.encodeValue(a, out) + codec.encodeValue(field.get(z), out) + } else { (z, out) => + field.foreachUnlessDefault(z) { (a: A) => + writeLabel(out) + codec.encodeValue(a, out) + } } } - } private type Fields[Z] = Vector[Field[Z, _]] private type LabelledFields[Z] = Vector[(Field[Z, _], String, Any)] private def labelledFields[Z](fields: Fields[Z]): LabelledFields[Z] = fields.map { field => - val jLabel = jsonLabel(field) + val jsonLabel = jsonLabelOrLabel(field) val decoded: Option[Any] = field.schema.getDefaultValue val default = decoded.orNull - (field, jLabel, default) + (field, jsonLabel, default) } private def nonPayloadStruct[Z]( @@ -1339,73 +1357,102 @@ private[smithy4s] class SchemaVisitorJCodec( ): JCodec[Z] = new JCodec[Z] { - private[this] val handlers = - new util.HashMap[String, Handler](fields.length << 1, 0.5f) { - fields.foreach { case (field, jLabel, _) => - put(jLabel, fieldHandler(field)) + private[this] val ( + knownFields: LabelledFields[Z], + unknownFieldRetainers: LabelledFields[Z] + ) = fields.partition { case (field, _, _) => + !isForUnknownFieldRetention(field) + } + + private[this] val knownFieldsHandlers = + new util.HashMap[String, Handler](knownFields.length << 1, 0.5f) { + knownFields.foreach { case (field, jsonLabel, _) => + put(jsonLabel, fieldHandler(field)) } } + private[this] val unknownFieldRetainerHandlers = + new util.HashMap[String, Handler]( + unknownFieldRetainers.length << 1, + 0.5f + ) { + unknownFieldRetainers.foreach { case (field, _, _) => + put(field.label, + // TODO: Change this to solve the TODO further down + fieldHandler(field)) + } + } + + private[this] val retainUnknownFields = + !unknownFieldRetainerHandlers.isEmpty + private[this] val documentEncoders = fields.map(labelledField => fieldEncoder(labelledField._1)) - def expecting: String = "object" + override def expecting: String = "object" override def canBeKey = false - def decodeValue(cursor: Cursor, in: JsonReader): Z = - decodeValue_(cursor, in)(emptyMetadata) - - private def decodeValue_( - cursor: Cursor, - in: JsonReader - ): scala.collection.Map[String, Any] => Z = { - val buffer = new util.HashMap[String, Any](handlers.size << 1, 0.5f) + override def decodeValue(cursor: Cursor, in: JsonReader): Z = { + val unknownFieldValues = + if (retainUnknownFields) new util.HashMap[String, Document] + else null + val knownFieldValues = + new util.HashMap[String, Any](knownFieldsHandlers.size << 1, 0.5f) if (in.isNextToken('{')) { - // In this case, metadata and payload are mixed together - // and values field values must be sought from either. if (!in.isNextToken('}')) { in.rollbackToken() while ({ - val handler = handlers.get(in.readKeyAsString()) - if (handler eq null) in.skip() - else handler(cursor, in, buffer) + val key = in.readKeyAsString() + val handler = knownFieldsHandlers.get(key) + if (handler eq null) + if (retainUnknownFields) { + val value = documentJCodec.decodeValue(cursor, in) + unknownFieldValues.put(key, value) + } else in.skip() + else handler(cursor, in, knownFieldValues) in.isNextToken(',') }) () if (!in.isCurrentToken('}')) in.objectEndOrCommaError() } } else in.decodeError("Expected JSON object") - - // At this point, we have parsed the json and retrieved - // all the values that interest us for the construction - // of our domain object. - // We therefore reconcile the values pulled from the json - // with the ones pull the metadata, and call the constructor - // on it. - { (meta: scala.collection.Map[String, Any]) => - meta.foreach(kv => buffer.put(kv._1, kv._2)) - val stage2 = new VectorBuilder[Any] - fields.foreach { case (f, jsonLabel, default) => - stage2 += { - val value = buffer.get(f.label) + val values = new VectorBuilder[Any] + fields.foreach { case (field, jsonLabel, default) => + if (!isForUnknownFieldRetention(field)) { + values += { + val value = knownFieldValues.get(field.label) if (value == null) { if (default == null) cursor.requiredFieldError(jsonLabel, jsonLabel) else default } else value } + } else { + values += { + // TODO: Lift out. + val unknownFieldsDecoder = + Document.Decoder.fromSchema(field.schema) + unknownFieldsDecoder + .decode(Document.DObject(unknownFieldValues.asScala.toMap)) + .getOrElse( + cursor.payloadError( + this, + "Expected JSON document" + ) + ) + } } - const(stage2.result()) } + const(values.result()) } - def encodeValue(z: Z, out: JsonWriter): Unit = + override def encodeValue(z: Z, out: JsonWriter): Unit = encode(z, out, documentEncoders) - def decodeKey(in: JsonReader): Z = + override def decodeKey(in: JsonReader): Z = in.decodeError("Cannot use products as keys") - def encodeKey(x: Z, out: JsonWriter): Unit = + override def encodeKey(x: Z, out: JsonWriter): Unit = out.encodeError("Cannot use products as keys") } diff --git a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala index 1a21e3551..1d4fe27f5 100644 --- a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala +++ b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala @@ -26,13 +26,17 @@ import smithy4s.codecs.PayloadError import smithy4s.codecs.PayloadPath import smithy4s.example.CheckedOrUnchecked import smithy4s.example.CheckedOrUnchecked2 +import smithy4s.example.DefaultRequiredUnknownFieldRetentionExample +import smithy4s.example.DefaultUnknownFieldRetentionExample import smithy4s.example.FaceCard import smithy4s.example.Four import smithy4s.example.One import smithy4s.example.PayloadData import smithy4s.example.RangeCheck +import smithy4s.example.RequiredUnknownFieldRetentionExample import smithy4s.example.TestBiggerUnion import smithy4s.example.Three +import smithy4s.example.UnknownFieldRetentionExample import smithy4s.example.UntaggedUnion import smithy4s.example.{OpenEnumTest, OpenIntEnumTest} import smithy4s.schema.Schema._ @@ -645,4 +649,93 @@ class SchemaVisitorJCodecTests() extends FunSuite { assertEquals(fromJson, Right(patchable)) } + test( + "unknown field retention + explicit defaults encoding = false" + ) { + val unknownFieldRetentionExample = UnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Some( + Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + ) + + val json = writeToString(unknownFieldRetentionExample) + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" + + val roundTripped = readFromString[UnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, unknownFieldRetentionExample) + } + + test( + "unknown field retention + explicit defaults encoding = true" + ) { + val defaultUnknownFieldRetentionExample = + DefaultUnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + + val json = writeToString(defaultUnknownFieldRetentionExample) + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" + + val roundTripped = readFromString[DefaultUnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, defaultUnknownFieldRetentionExample) + } + + test( + "required unknown field retention + explicit defaults encoding = false" + ) { + val requiredUnknownFieldRetentionExample = + RequiredUnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + + val json = writeToString(requiredUnknownFieldRetentionExample) + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" + + val roundTripped = readFromString[RequiredUnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, requiredUnknownFieldRetentionExample) + } + + test( + "required unknown field retention + explicit defaults encoding = true" + ) { + val defaultRequiredUnknownFieldRetentionExample = + DefaultRequiredUnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + retainedUnknownFields = Document.obj( + "unknownField1" -> Document.fromString("unknownString1"), + "unknownField2" -> Document.fromString("unknownString2") + ) + ) + + val json = writeToString(defaultRequiredUnknownFieldRetentionExample) + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" + + val roundTripped = readFromString[DefaultRequiredUnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, defaultRequiredUnknownFieldRetentionExample) + } + } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ff10f817d..687fbd56c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -30,7 +30,7 @@ object Dependencies { val Alloy = new { val org = "com.disneystreaming.alloy" - val alloyVersion = "0.3.2" + val alloyVersion = "0.3.2-6-a741ba-SNAPSHOT" val core = org % "alloy-core" % alloyVersion val openapi = org %% "alloy-openapi" % alloyVersion val `protocol-tests` = org % "alloy-protocol-tests" % alloyVersion diff --git a/project/Smithy4sBuildPlugin.scala b/project/Smithy4sBuildPlugin.scala index f44bb275c..36f54ffec 100644 --- a/project/Smithy4sBuildPlugin.scala +++ b/project/Smithy4sBuildPlugin.scala @@ -40,6 +40,7 @@ object Smithy4sBuildPlugin extends AutoPlugin { val genSmithyResourcesOutput = SettingKey[File]("genSmithyResourcesOutput") val allowedNamespaces = SettingKey[Seq[String]]("allowedNamespaces") val smithy4sModelTransformers = SettingKey[Seq[String]]("smithy4sModelTransformers") + val smithy4sRepositories = SettingKey[Seq[String]]("smithy4sRepositories") val smithy4sDependencies = SettingKey[Seq[ModuleID]]("smithy4sDependencies") val smithy4sSkip = SettingKey[Seq[String]]("smithy4sSkip") val bloopEnabled = SettingKey[Boolean]("bloopEnabled") @@ -86,6 +87,11 @@ object Smithy4sBuildPlugin extends AutoPlugin { override def buildSettings: Seq[Setting[_]] = Seq( smithySpecs := Seq.empty, + // TODO: Remove once https://github.com/disneystreaming/alloy/pull/135 is + // merged and released. + smithy4sRepositories := Seq( + "https://s01.oss.sonatype.org/content/repositories/snapshots" + ), smithy4sDependencies := Seq(Dependencies.Alloy.core), bloopAllowedCombos := Seq( Seq(VirtualAxis.jvm, VirtualAxis.scalaABIVersion(Scala213)) diff --git a/project/build.properties b/project/build.properties index 72413de15..abbbce5da 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.3 +sbt.version=1.9.8 diff --git a/sampleSpecs/unknownFieldRetention.smithy b/sampleSpecs/unknownFieldRetention.smithy new file mode 100644 index 000000000..9cb33e82b --- /dev/null +++ b/sampleSpecs/unknownFieldRetention.smithy @@ -0,0 +1,42 @@ +$version: "2" + +namespace smithy4s.example + +use alloy#unknownDocumentFieldRetention +use alloy#unknownJsonFieldRetention + +structure UnknownFieldRetentionExample { + foo: String + bar: String + @unknownDocumentFieldRetention + @unknownJsonFieldRetention + retainedUnknownFields: Document +} + +structure DefaultUnknownFieldRetentionExample { + foo: String + bar: String + @default + @unknownDocumentFieldRetention + @unknownJsonFieldRetention + retainedUnknownFields: Document +} + +structure RequiredUnknownFieldRetentionExample { + foo: String + bar: String + @required + @unknownDocumentFieldRetention + @unknownJsonFieldRetention + retainedUnknownFields: Document +} + +structure DefaultRequiredUnknownFieldRetentionExample { + foo: String + bar: String + @default + @required + @unknownDocumentFieldRetention + @unknownJsonFieldRetention + retainedUnknownFields: Document +}