From aa08e4a1733efa00c911a1f1badc26f4666a0ae7 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Tue, 13 Feb 2024 17:28:57 +0000 Subject: [PATCH 01/17] Build against alloy 0.2.8-27-e53667-SNAPSHOT --- build.sbt | 41 +++++++++++++++++-- .../com/amazonaws/dynamodb/Endpoint.scala | 2 +- .../smithy4s/example/EnumResult.scala | 4 +- .../generated/smithy4s/example/FaceCard.scala | 1 + .../generated/smithy4s/example/Numbers.scala | 4 +- .../generated/smithy4s/example/OneTwo.scala | 4 +- .../example/OpenIntEnumCollisionTest.scala | 1 + .../example/OpenIntEnumCollisionTest2.scala | 1 + .../smithy4s/example/OpenIntEnumTest.scala | 1 + .../generated/smithy4s/example/OpenNums.scala | 1 + .../smithy4s/example/OperationInput.scala | 2 +- .../guides/auth/HelloWorldAuthService.scala | 2 +- .../src/smithy4s/codegen/cli/Main.scala | 11 +++-- project/Dependencies.scala | 2 +- project/Smithy4sBuildPlugin.scala | 6 +++ project/build.properties | 2 +- 16 files changed, 68 insertions(+), 17 deletions(-) diff --git a/build.sbt b/build.sbt index 78c93dd55..e2c664caf 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.Alloy.`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, @@ -963,7 +984,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") @@ -975,6 +999,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 @@ -1022,8 +1048,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") @@ -1076,6 +1104,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 => @@ -1153,6 +1183,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(",")) @@ -1162,6 +1196,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 42b407cfe..3c5620a07 100644 --- a/modules/bootstrapped/src/generated/com/amazonaws/dynamodb/Endpoint.scala +++ b/modules/bootstrapped/src/generated/com/amazonaws/dynamodb/Endpoint.scala @@ -25,7 +25,7 @@ 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))), ){ Endpoint.apply }.withId(id).addHints(hints) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/EnumResult.scala b/modules/bootstrapped/src/generated/smithy4s/example/EnumResult.scala index d85479ce0..c6dbbc272 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/EnumResult.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/EnumResult.scala @@ -20,7 +20,9 @@ sealed abstract class EnumResult(_value: String, _name: String, _intValue: Int, object EnumResult extends Enumeration[EnumResult] with ShapeTag.Companion[EnumResult] { val id: ShapeId = ShapeId("smithy4s.example", "EnumResult") - val hints: Hints = Hints.empty + val hints: Hints = Hints( + smithy.api.Box(), + ).lazily case object FIRST extends EnumResult("FIRST", "FIRST", 1, Hints.empty) case object SECOND extends EnumResult("SECOND", "SECOND", 2, Hints.empty) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/FaceCard.scala b/modules/bootstrapped/src/generated/smithy4s/example/FaceCard.scala index d8caf2516..75c83c7f3 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/FaceCard.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/FaceCard.scala @@ -22,6 +22,7 @@ object FaceCard extends Enumeration[FaceCard] with ShapeTag.Companion[FaceCard] val id: ShapeId = ShapeId("smithy4s.example", "FaceCard") val hints: Hints = Hints( + smithy.api.Box(), smithy.api.Documentation("FaceCard types"), ).lazily diff --git a/modules/bootstrapped/src/generated/smithy4s/example/Numbers.scala b/modules/bootstrapped/src/generated/smithy4s/example/Numbers.scala index cf824a9ae..296fe4f4e 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/Numbers.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/Numbers.scala @@ -20,7 +20,9 @@ sealed abstract class Numbers(_value: String, _name: String, _intValue: Int, _hi object Numbers extends Enumeration[Numbers] with ShapeTag.Companion[Numbers] { val id: ShapeId = ShapeId("smithy4s.example", "Numbers") - val hints: Hints = Hints.empty + val hints: Hints = Hints( + smithy.api.Box(), + ).lazily case object ONE extends Numbers("ONE", "ONE", 1, Hints.empty) case object TWO extends Numbers("TWO", "TWO", 2, Hints.empty) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OneTwo.scala b/modules/bootstrapped/src/generated/smithy4s/example/OneTwo.scala index 4fc7efde4..cb7a46f02 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OneTwo.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OneTwo.scala @@ -20,7 +20,9 @@ sealed abstract class OneTwo(_value: String, _name: String, _intValue: Int, _hin object OneTwo extends Enumeration[OneTwo] with ShapeTag.Companion[OneTwo] { val id: ShapeId = ShapeId("smithy4s.example", "oneTwo") - val hints: Hints = Hints.empty + val hints: Hints = Hints( + smithy.api.Box(), + ).lazily case object ONE extends OneTwo("ONE", "ONE", 1, Hints.empty) case object TWO extends OneTwo("TWO", "TWO", 2, Hints.empty) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest.scala b/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest.scala index 3bc4c2840..a40097090 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest.scala @@ -22,6 +22,7 @@ object OpenIntEnumCollisionTest extends Enumeration[OpenIntEnumCollisionTest] wi val id: ShapeId = ShapeId("smithy4s.example", "OpenIntEnumCollisionTest") val hints: Hints = Hints( + smithy.api.Box(), alloy.OpenEnum(), ).lazily diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest2.scala b/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest2.scala index 0288a9d5b..be96c1e8b 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest2.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumCollisionTest2.scala @@ -22,6 +22,7 @@ object OpenIntEnumCollisionTest2 extends Enumeration[OpenIntEnumCollisionTest2] val id: ShapeId = ShapeId("smithy4s.example", "OpenIntEnumCollisionTest2") val hints: Hints = Hints( + smithy.api.Box(), alloy.OpenEnum(), ).lazily diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumTest.scala b/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumTest.scala index 62decb009..3e15b3cdb 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumTest.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OpenIntEnumTest.scala @@ -22,6 +22,7 @@ object OpenIntEnumTest extends Enumeration[OpenIntEnumTest] with ShapeTag.Compan val id: ShapeId = ShapeId("smithy4s.example", "OpenIntEnumTest") val hints: Hints = Hints( + smithy.api.Box(), alloy.OpenEnum(), ).lazily diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OpenNums.scala b/modules/bootstrapped/src/generated/smithy4s/example/OpenNums.scala index 005a93fc8..a162176db 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OpenNums.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OpenNums.scala @@ -21,6 +21,7 @@ object OpenNums extends Enumeration[OpenNums] with ShapeTag.Companion[OpenNums] val id: ShapeId = ShapeId("smithy4s.example", "OpenNums") val hints: Hints = Hints( + smithy.api.Box(), alloy.OpenEnum(), ).lazily diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala b/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala index 6640a2556..5ee23c339 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OperationInput.scala @@ -18,7 +18,7 @@ object OperationInput extends ShapeTag.Companion[OperationInput] { implicit val schema: Schema[OperationInput] = struct( 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.field[OperationInput]("optionalHeaderWithDefault", _.optionalHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-header-with-default")), smithy.api.HttpHeader("optional-header-with-default")), string.required[OperationInput]("requiredHeaderWithDefault", _.requiredHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-header-with-default")), smithy.api.HttpHeader("required-header-with-default")), 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/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/project/Dependencies.scala b/project/Dependencies.scala index 5332958be..036a54bd9 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -29,7 +29,7 @@ object Dependencies { val Alloy = new { val org = "com.disneystreaming.alloy" - val alloyVersion = "0.2.8" + val alloyVersion = "0.2.8-27-e53667-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 790378586..50611f7c4 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 From f1cf193f9670426ea0ecdb9dc763867bbcd4652c Mon Sep 17 00:00:00 2001 From: David Piggott Date: Thu, 15 Feb 2024 13:57:03 +0000 Subject: [PATCH 02/17] First pass of Document codec support --- ...RequiredUnknownFieldRetentionExample.scala | 25 ++++ .../DefaultUnknownFieldRetentionExample.scala | 25 ++++ ...RequiredUnknownFieldRetentionExample.scala | 25 ++++ .../example/RetainedUnknownFields.scala | 18 +++ .../UnknownFieldRetentionExample.scala | 25 ++++ .../generated/smithy4s/example/package.scala | 1 + .../test/src/smithy4s/DocumentSpec.scala | 137 +++++++++++++++++- .../DocumentDecoderSchemaVisitor.scala | 118 +++++++++------ .../DocumentEncoderSchemaVisitor.scala | 51 ++++--- sampleSpecs/unknownFieldRetention.smithy | 42 ++++++ 10 files changed, 407 insertions(+), 60 deletions(-) create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala create mode 100644 sampleSpecs/unknownFieldRetention.smithy 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..aae85a91e --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala @@ -0,0 +1,25 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class DefaultRequiredUnknownFieldRetentionExample(bazes: Map[String, Document] = Map(), 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 + + implicit val schema: Schema[DefaultRequiredUnknownFieldRetentionExample] = struct( + RetainedUnknownFields.underlyingSchema.required[DefaultRequiredUnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.obj())), + string.optional[DefaultRequiredUnknownFieldRetentionExample]("foo", _.foo), + string.optional[DefaultRequiredUnknownFieldRetentionExample]("bar", _.bar), + ){ + DefaultRequiredUnknownFieldRetentionExample.apply + }.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..52e9b901d --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala @@ -0,0 +1,25 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class DefaultUnknownFieldRetentionExample(bazes: Map[String, Document] = Map(), 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 + + implicit val schema: Schema[DefaultUnknownFieldRetentionExample] = struct( + RetainedUnknownFields.underlyingSchema.field[DefaultUnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.obj())), + string.optional[DefaultUnknownFieldRetentionExample]("foo", _.foo), + string.optional[DefaultUnknownFieldRetentionExample]("bar", _.bar), + ){ + DefaultUnknownFieldRetentionExample.apply + }.withId(id).addHints(hints) +} 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..937f69b9f --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala @@ -0,0 +1,25 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class RequiredUnknownFieldRetentionExample(bazes: Map[String, 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 + + implicit val schema: Schema[RequiredUnknownFieldRetentionExample] = struct( + RetainedUnknownFields.underlyingSchema.required[RequiredUnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention()), + string.optional[RequiredUnknownFieldRetentionExample]("foo", _.foo), + string.optional[RequiredUnknownFieldRetentionExample]("bar", _.bar), + ){ + RequiredUnknownFieldRetentionExample.apply + }.withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala b/modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala new file mode 100644 index 000000000..f53ca1059 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala @@ -0,0 +1,18 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Newtype +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.schema.Schema.bijection +import smithy4s.schema.Schema.document +import smithy4s.schema.Schema.map +import smithy4s.schema.Schema.string + +object RetainedUnknownFields extends Newtype[Map[String, Document]] { + val id: ShapeId = ShapeId("smithy4s.example", "RetainedUnknownFields") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[Map[String, Document]] = map(string, document).withId(id).addHints(hints) + implicit val schema: Schema[RetainedUnknownFields] = bijection(underlyingSchema, asBijection) +} 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..f5cecc0cd --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala @@ -0,0 +1,25 @@ +package smithy4s.example + +import smithy4s.Document +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.struct + +final case class UnknownFieldRetentionExample(foo: Option[String] = None, bar: Option[String] = None, bazes: Option[Map[String, Document]] = None) + +object UnknownFieldRetentionExample extends ShapeTag.Companion[UnknownFieldRetentionExample] { + val id: ShapeId = ShapeId("smithy4s.example", "UnknownFieldRetentionExample") + + val hints: Hints = Hints.empty + + implicit val schema: Schema[UnknownFieldRetentionExample] = struct( + string.optional[UnknownFieldRetentionExample]("foo", _.foo), + string.optional[UnknownFieldRetentionExample]("bar", _.bar), + RetainedUnknownFields.underlyingSchema.optional[UnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention()), + ){ + UnknownFieldRetentionExample.apply + }.withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/package.scala b/modules/bootstrapped/src/generated/smithy4s/example/package.scala index 900b37050..eb6fb025f 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/package.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/package.scala @@ -72,6 +72,7 @@ package object example { type DogName = smithy4s.example.DogName.Type type TestIdRefTwo = smithy4s.example.TestIdRefTwo.Type type FancyList = smithy4s.example.FancyList.Type + type RetainedUnknownFields = smithy4s.example.RetainedUnknownFields.Type type Ingredients = smithy4s.example.Ingredients.Type type StringList = smithy4s.example.StringList.Type /** Multiple line doc comment for another string diff --git a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala index 93f85b226..e1c4123cf 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.IntList import smithy4s.example.OperationOutput +import smithy4s.example.DefaultRequiredUnknownFieldRetentionExample +import smithy4s.example.DefaultUnknownFieldRetentionExample +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" ) { @@ -540,4 +545,134 @@ class DocumentSpec() extends FunSuite { } + test( + "document codec - unknown field retention + explicit defaults encoding = false" + ) { + val unknownFieldRetentionExample = UnknownFieldRetentionExample( + foo = Some("foo"), + bar = Some("bar"), + bazes = Some( + Map( + "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"), + bazes = Map( + "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"), + bazes = Map( + "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"), + bazes = Map( + "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/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index e12577f13..81b87418d 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -18,6 +18,7 @@ package smithy4s package internals import alloy.Discriminated +import alloy.UnknownFieldRetention import smithy.api.JsonName import smithy.api.TimestampFormat import smithy.api.TimestampFormat.DATE_TIME @@ -311,56 +312,89 @@ 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(UnknownFieldRetention) + 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) + ) => Unit = + if (isForUnknownFieldRetention(field)) { + field.schema match { + case Schema.MapSchema(_, _, Schema.string, Schema.document) => + ( + _: List[PayloadPath.Segment], + buffer: Any => Unit, + fields: Map[String, Document] + ) => { + val retainedUnknownFields = fields -- knownFieldLabels + buffer(retainedUnknownFields) } - 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 => - throw new PayloadError( - PayloadPath(path.reverse), - "", - "Required field not found" - ) + + case Schema.OptionSchema( + Schema.MapSchema(_, _, Schema.string, Schema.document) + ) => + ( + _: List[PayloadPath.Segment], + buffer: Any => Unit, + fields: Map[String, Document] + ) => { + val retainedUnknownFields = fields -- knownFieldLabels + buffer(Some(retainedUnknownFields)) } - } - } - val fieldDecoders = fields.map(field => fieldDecoder(field)) + case _ => + ( + _: List[PayloadPath.Segment], + _: Any => Unit, + _: Map[String, Document] + ) => () + } + } else { + val jsonLabel = jsonLabelOrLabel(field) + field.getDefaultValue match { + case Some(defaultValue) => + ( + pp: List[PayloadPath.Segment], + buffer: Any => Unit, + fields: Map[String, Document] + ) => { + 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: List[PayloadPath.Segment], + buffer: Any => Unit, + fields: Map[String, Document] + ) => { + 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] diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index 37204c562..d8125e877 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.UnknownFieldRetention +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,42 @@ 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(UnknownFieldRetention) + ) { + field.schema match { + case Schema.MapSchema(_, _, Schema.string, Schema.document) => + (s, builder) => builder ++= field.get(s) + + case Schema.OptionSchema( + Schema.MapSchema(_, _, Schema.string, Schema.document) + ) => + (s, builder) => field.get(s).foreach(builder ++= _) - val encoders = fields.map(field => fieldEncoder(field)) + case _ => + (_, _) => () + } + } 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/sampleSpecs/unknownFieldRetention.smithy b/sampleSpecs/unknownFieldRetention.smithy new file mode 100644 index 000000000..83b2b79c7 --- /dev/null +++ b/sampleSpecs/unknownFieldRetention.smithy @@ -0,0 +1,42 @@ +$version: "2" + +namespace smithy4s.example + +use alloy#unknownFieldRetention + +structure UnknownFieldRetentionExample { + foo: String + bar: String + @unknownFieldRetention + bazes: RetainedUnknownFields + } + +structure DefaultUnknownFieldRetentionExample { + foo: String + bar: String + @default + @unknownFieldRetention + bazes: RetainedUnknownFields + } + +structure RequiredUnknownFieldRetentionExample { + foo: String + bar: String + @required + @unknownFieldRetention + bazes: RetainedUnknownFields + } + +structure DefaultRequiredUnknownFieldRetentionExample { + foo: String + bar: String + @default + @required + @unknownFieldRetention + bazes: RetainedUnknownFields + } + + map RetainedUnknownFields { + key: String + value: Document +} From f3d9be9ebd887315afcd6244a06109afb730a11b Mon Sep 17 00:00:00 2001 From: David Piggott Date: Wed, 21 Feb 2024 13:01:37 +0000 Subject: [PATCH 03/17] Second pass of Document codec support --- ...RequiredUnknownFieldRetentionExample.scala | 5 +- .../DefaultUnknownFieldRetentionExample.scala | 5 +- ...RequiredUnknownFieldRetentionExample.scala | 5 +- .../example/RetainedUnknownFields.scala | 18 ---- .../UnknownFieldRetentionExample.scala | 5 +- .../generated/smithy4s/example/package.scala | 1 - .../test/src/smithy4s/DocumentSpec.scala | 10 +-- .../DocumentDecoderSchemaVisitor.scala | 90 ++++++++++++------- .../DocumentEncoderSchemaVisitor.scala | 50 ++++++++--- project/Dependencies.scala | 2 +- sampleSpecs/unknownFieldRetention.smithy | 19 ++-- 11 files changed, 123 insertions(+), 87 deletions(-) delete mode 100644 modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala index aae85a91e..ac369e94b 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala @@ -5,10 +5,11 @@ 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(bazes: Map[String, Document] = Map(), foo: Option[String] = None, bar: Option[String] = None) +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") @@ -16,7 +17,7 @@ object DefaultRequiredUnknownFieldRetentionExample extends ShapeTag.Companion[De val hints: Hints = Hints.empty implicit val schema: Schema[DefaultRequiredUnknownFieldRetentionExample] = struct( - RetainedUnknownFields.underlyingSchema.required[DefaultRequiredUnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.obj())), + document.required[DefaultRequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), string.optional[DefaultRequiredUnknownFieldRetentionExample]("foo", _.foo), string.optional[DefaultRequiredUnknownFieldRetentionExample]("bar", _.bar), ){ diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala index 52e9b901d..f7028af70 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala @@ -5,10 +5,11 @@ 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(bazes: Map[String, Document] = Map(), foo: Option[String] = None, bar: Option[String] = None) +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") @@ -16,7 +17,7 @@ object DefaultUnknownFieldRetentionExample extends ShapeTag.Companion[DefaultUnk val hints: Hints = Hints.empty implicit val schema: Schema[DefaultUnknownFieldRetentionExample] = struct( - RetainedUnknownFields.underlyingSchema.field[DefaultUnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.obj())), + document.field[DefaultUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), string.optional[DefaultUnknownFieldRetentionExample]("foo", _.foo), string.optional[DefaultUnknownFieldRetentionExample]("bar", _.bar), ){ diff --git a/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala index 937f69b9f..fa4cbdbd9 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala @@ -5,10 +5,11 @@ 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(bazes: Map[String, Document], foo: Option[String] = None, bar: Option[String] = None) +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") @@ -16,7 +17,7 @@ object RequiredUnknownFieldRetentionExample extends ShapeTag.Companion[RequiredU val hints: Hints = Hints.empty implicit val schema: Schema[RequiredUnknownFieldRetentionExample] = struct( - RetainedUnknownFields.underlyingSchema.required[RequiredUnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention()), + document.required[RequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention()), string.optional[RequiredUnknownFieldRetentionExample]("foo", _.foo), string.optional[RequiredUnknownFieldRetentionExample]("bar", _.bar), ){ diff --git a/modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala b/modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala deleted file mode 100644 index f53ca1059..000000000 --- a/modules/bootstrapped/src/generated/smithy4s/example/RetainedUnknownFields.scala +++ /dev/null @@ -1,18 +0,0 @@ -package smithy4s.example - -import smithy4s.Document -import smithy4s.Hints -import smithy4s.Newtype -import smithy4s.Schema -import smithy4s.ShapeId -import smithy4s.schema.Schema.bijection -import smithy4s.schema.Schema.document -import smithy4s.schema.Schema.map -import smithy4s.schema.Schema.string - -object RetainedUnknownFields extends Newtype[Map[String, Document]] { - val id: ShapeId = ShapeId("smithy4s.example", "RetainedUnknownFields") - val hints: Hints = Hints.empty - val underlyingSchema: Schema[Map[String, Document]] = map(string, document).withId(id).addHints(hints) - implicit val schema: Schema[RetainedUnknownFields] = bijection(underlyingSchema, asBijection) -} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala index f5cecc0cd..c7d33b4ad 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala @@ -5,10 +5,11 @@ 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, bazes: Option[Map[String, Document]] = None) +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") @@ -18,7 +19,7 @@ object UnknownFieldRetentionExample extends ShapeTag.Companion[UnknownFieldReten implicit val schema: Schema[UnknownFieldRetentionExample] = struct( string.optional[UnknownFieldRetentionExample]("foo", _.foo), string.optional[UnknownFieldRetentionExample]("bar", _.bar), - RetainedUnknownFields.underlyingSchema.optional[UnknownFieldRetentionExample]("bazes", _.bazes).addHints(alloy.UnknownFieldRetention()), + document.optional[UnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention()), ){ UnknownFieldRetentionExample.apply }.withId(id).addHints(hints) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/package.scala b/modules/bootstrapped/src/generated/smithy4s/example/package.scala index eb6fb025f..900b37050 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/package.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/package.scala @@ -72,7 +72,6 @@ package object example { type DogName = smithy4s.example.DogName.Type type TestIdRefTwo = smithy4s.example.TestIdRefTwo.Type type FancyList = smithy4s.example.FancyList.Type - type RetainedUnknownFields = smithy4s.example.RetainedUnknownFields.Type type Ingredients = smithy4s.example.Ingredients.Type type StringList = smithy4s.example.StringList.Type /** Multiple line doc comment for another string diff --git a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala index e1c4123cf..51ca00bf1 100644 --- a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala @@ -551,8 +551,8 @@ class DocumentSpec() extends FunSuite { val unknownFieldRetentionExample = UnknownFieldRetentionExample( foo = Some("foo"), bar = Some("bar"), - bazes = Some( - Map( + retainedUnknownFields = Some( + Document.obj( "unknownField1" -> Document.fromString("unknownString1"), "unknownField2" -> Document.fromString("unknownString2") ) @@ -583,7 +583,7 @@ class DocumentSpec() extends FunSuite { DefaultUnknownFieldRetentionExample( foo = Some("foo"), bar = Some("bar"), - bazes = Map( + retainedUnknownFields = Document.obj( "unknownField1" -> Document.fromString("unknownString1"), "unknownField2" -> Document.fromString("unknownString2") ) @@ -615,7 +615,7 @@ class DocumentSpec() extends FunSuite { RequiredUnknownFieldRetentionExample( foo = Some("foo"), bar = Some("bar"), - bazes = Map( + retainedUnknownFields = Document.obj( "unknownField1" -> Document.fromString("unknownString1"), "unknownField2" -> Document.fromString("unknownString2") ) @@ -647,7 +647,7 @@ class DocumentSpec() extends FunSuite { DefaultRequiredUnknownFieldRetentionExample( foo = Some("foo"), bar = Some("bar"), - bazes = Map( + retainedUnknownFields = Document.obj( "unknownField1" -> Document.fromString("unknownString1"), "unknownField2" -> Document.fromString("unknownString2") ) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index 81b87418d..77959a56e 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -306,6 +306,57 @@ class DocumentDecoderSchemaVisitor( } } + trait UnknownFieldsDecoder[A] { self => + def apply( + history: List[PayloadPath.Segment], + unknownFields: Map[String, Document] + ): A + } + + object UnknownFieldsDecoder + extends SchemaVisitor.Default[UnknownFieldsDecoder] { self => + + override def default[A]: UnknownFieldsDecoder[A] = ( + history: List[PayloadPath.Segment], + _: Map[String, Document] + ) => + throw PayloadError( + PayloadPath(history.reverse), + "Json document", + "Expected Json Shape: Object" + ) + + override def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ): UnknownFieldsDecoder[P] = ( + history: List[PayloadPath.Segment], + unknownFields: Map[String, Document] + ) => + tag match { + case PDocument => Document.DObject(unknownFields) + case _ => + throw PayloadError( + PayloadPath(history.reverse), + "Json document", + "Expected Json Shape: Object" + ) + } + + override def option[A]( + schema: Schema[A] + ): UnknownFieldsDecoder[Option[A]] = { + val decoder = schema.compile(self) + ( + history: List[PayloadPath.Segment], + unknownFields: Map[String, Document] + ) => + if (unknownFields.isEmpty) None + else Some(decoder(history, unknownFields)) + } + } + override def struct[S]( shapeId: ShapeId, hints: Hints, @@ -324,35 +375,14 @@ class DocumentDecoderSchemaVisitor( Map[String, Document] ) => Unit = if (isForUnknownFieldRetention(field)) { - field.schema match { - case Schema.MapSchema(_, _, Schema.string, Schema.document) => - ( - _: List[PayloadPath.Segment], - buffer: Any => Unit, - fields: Map[String, Document] - ) => { - val retainedUnknownFields = fields -- knownFieldLabels - buffer(retainedUnknownFields) - } - - case Schema.OptionSchema( - Schema.MapSchema(_, _, Schema.string, Schema.document) - ) => - ( - _: List[PayloadPath.Segment], - buffer: Any => Unit, - fields: Map[String, Document] - ) => { - val retainedUnknownFields = fields -- knownFieldLabels - buffer(Some(retainedUnknownFields)) - } - - case _ => - ( - _: List[PayloadPath.Segment], - _: Any => Unit, - _: Map[String, Document] - ) => () + val unknownFieldsDecoder = UnknownFieldsDecoder(field.schema) + ( + pp: List[PayloadPath.Segment], + buffer: Any => Unit, + fields: Map[String, Document] + ) => { + val unknownFields = fields -- knownFieldLabels + buffer(unknownFieldsDecoder(pp, unknownFields)) } } else { val jsonLabel = jsonLabelOrLabel(field) @@ -398,7 +428,7 @@ class DocumentDecoderSchemaVisitor( 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()) } } diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index d8125e877..8961ebfb1 100644 --- a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala @@ -182,6 +182,42 @@ class DocumentEncoderSchemaVisitor( from(e => DString(total(e).stringValue)) } + trait UnknownFieldsEncoder[A] { + def apply(a: A): Map[String, Document] + } + + object UnknownFieldsEncoder + extends SchemaVisitor.Default[UnknownFieldsEncoder] { self => + + override def default[A]: UnknownFieldsEncoder[A] = _ => Map.empty + + override def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ): UnknownFieldsEncoder[P] = document => + tag match { + case PDocument => + document match { + case Document.DObject(values) => values + case _ => Map.empty + } + + case _ => + Map.empty + } + + override def option[A]( + schema: Schema[A] + ): UnknownFieldsEncoder[Option[A]] = { + val encoder = self(schema) + locally { + case Some(a) => encoder.apply(a) + case None => Map.empty + } + } + } + override def struct[S]( shapeId: ShapeId, hints: Hints, @@ -201,18 +237,8 @@ class DocumentEncoderSchemaVisitor( field.hints .has(UnknownFieldRetention) ) { - field.schema match { - case Schema.MapSchema(_, _, Schema.string, Schema.document) => - (s, builder) => builder ++= field.get(s) - - case Schema.OptionSchema( - Schema.MapSchema(_, _, Schema.string, Schema.document) - ) => - (s, builder) => field.get(s).foreach(builder ++= _) - - case _ => - (_, _) => () - } + val unknownFieldsEncoder = UnknownFieldsEncoder(field.schema) + (s, builder) => builder ++= unknownFieldsEncoder(field.get(s)) } else { val encoder = apply(field.schema) val jsonLabel = field.hints diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 036a54bd9..c8594e281 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -29,7 +29,7 @@ object Dependencies { val Alloy = new { val org = "com.disneystreaming.alloy" - val alloyVersion = "0.2.8-27-e53667-SNAPSHOT" + val alloyVersion = "0.2.8-29-706e86-SNAPSHOT" val core = org % "alloy-core" % alloyVersion val openapi = org %% "alloy-openapi" % alloyVersion val `protocol-tests` = org % "alloy-protocol-tests" % alloyVersion diff --git a/sampleSpecs/unknownFieldRetention.smithy b/sampleSpecs/unknownFieldRetention.smithy index 83b2b79c7..2ed85a297 100644 --- a/sampleSpecs/unknownFieldRetention.smithy +++ b/sampleSpecs/unknownFieldRetention.smithy @@ -8,24 +8,24 @@ structure UnknownFieldRetentionExample { foo: String bar: String @unknownFieldRetention - bazes: RetainedUnknownFields - } + retainedUnknownFields: Document +} structure DefaultUnknownFieldRetentionExample { foo: String bar: String @default @unknownFieldRetention - bazes: RetainedUnknownFields - } + retainedUnknownFields: Document +} structure RequiredUnknownFieldRetentionExample { foo: String bar: String @required @unknownFieldRetention - bazes: RetainedUnknownFields - } + retainedUnknownFields: Document +} structure DefaultRequiredUnknownFieldRetentionExample { foo: String @@ -33,10 +33,5 @@ structure DefaultRequiredUnknownFieldRetentionExample { @default @required @unknownFieldRetention - bazes: RetainedUnknownFields - } - - map RetainedUnknownFields { - key: String - value: Document + retainedUnknownFields: Document } From dd7b5ecdbbb038e4b15a78e204176a18848d4bf0 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Thu, 7 Mar 2024 11:09:58 +0000 Subject: [PATCH 04/17] Bump to alloy snapshot with unknownJsonFieldRetention trait --- .../DefaultRequiredUnknownFieldRetentionExample.scala | 2 +- .../example/DefaultUnknownFieldRetentionExample.scala | 2 +- .../example/RequiredUnknownFieldRetentionExample.scala | 2 +- .../example/UnknownFieldRetentionExample.scala | 2 +- .../internals/DocumentDecoderSchemaVisitor.scala | 4 ++-- .../internals/DocumentEncoderSchemaVisitor.scala | 4 ++-- project/Dependencies.scala | 2 +- sampleSpecs/unknownFieldRetention.smithy | 10 +++++----- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala index 8ced5c38c..b640639ad 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object DefaultRequiredUnknownFieldRetentionExample extends ShapeTag.Companion[De implicit val schema: Schema[DefaultRequiredUnknownFieldRetentionExample] = struct( string.optional[DefaultRequiredUnknownFieldRetentionExample]("foo", _.foo), string.optional[DefaultRequiredUnknownFieldRetentionExample]("bar", _.bar), - document.required[DefaultRequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), + document.required[DefaultRequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), )(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 index 8dc006896..35b869d54 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object DefaultUnknownFieldRetentionExample extends ShapeTag.Companion[DefaultUnk implicit val schema: Schema[DefaultUnknownFieldRetentionExample] = struct( string.optional[DefaultUnknownFieldRetentionExample]("foo", _.foo), string.optional[DefaultUnknownFieldRetentionExample]("bar", _.bar), - document.field[DefaultUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), + document.field[DefaultUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), )(make).withId(id).addHints(hints) } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala index 5df2d1c4d..ec7d20b63 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object RequiredUnknownFieldRetentionExample extends ShapeTag.Companion[RequiredU implicit val schema: Schema[RequiredUnknownFieldRetentionExample] = struct( string.optional[RequiredUnknownFieldRetentionExample]("foo", _.foo), string.optional[RequiredUnknownFieldRetentionExample]("bar", _.bar), - document.required[RequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention()), + document.required[RequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention()), )(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 index 1be0732f2..9591299d7 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object UnknownFieldRetentionExample extends ShapeTag.Companion[UnknownFieldReten implicit val schema: Schema[UnknownFieldRetentionExample] = struct( string.optional[UnknownFieldRetentionExample]("foo", _.foo), string.optional[UnknownFieldRetentionExample]("bar", _.bar), - document.optional[UnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownFieldRetention()), + document.optional[UnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention()), )(make).withId(id).addHints(hints) } diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index c18f4087d..ecd4a55f0 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -19,7 +19,7 @@ package internals import alloy.Discriminated import alloy.Nullable -import alloy.UnknownFieldRetention +import alloy.UnknownDocumentFieldRetention import smithy.api.JsonName import smithy.api.TimestampFormat import smithy.api.TimestampFormat.DATE_TIME @@ -366,7 +366,7 @@ class DocumentDecoderSchemaVisitor( make: IndexedSeq[Any] => S ): DocumentDecoder[S] = { def isForUnknownFieldRetention(field: Field[S, _]): Boolean = field.hints - .has(UnknownFieldRetention) + .has(UnknownDocumentFieldRetention) def jsonLabelOrLabel(field: Field[S, _]): String = field.hints.get(JsonName).map(_.value).getOrElse(field.label) val knownFieldLabels = diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index 8961ebfb1..68bc3055f 100644 --- a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala @@ -23,7 +23,7 @@ import smithy.api.TimestampFormat.DATE_TIME import smithy.api.TimestampFormat.EPOCH_SECONDS import smithy.api.TimestampFormat.HTTP_DATE import alloy.Discriminated -import alloy.UnknownFieldRetention +import alloy.UnknownDocumentFieldRetention import alloy.Untagged import smithy4s.capability.EncoderK import smithy4s.schema._ @@ -235,7 +235,7 @@ class DocumentEncoderSchemaVisitor( ): (S, Builder[(String, Document), Map[String, Document]]) => Unit = if ( field.hints - .has(UnknownFieldRetention) + .has(UnknownDocumentFieldRetention) ) { val unknownFieldsEncoder = UnknownFieldsEncoder(field.schema) (s, builder) => builder ++= unknownFieldsEncoder(field.get(s)) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ec88ac282..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-5-185b0b-SNAPSHOT" + 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/sampleSpecs/unknownFieldRetention.smithy b/sampleSpecs/unknownFieldRetention.smithy index 2ed85a297..3634c330a 100644 --- a/sampleSpecs/unknownFieldRetention.smithy +++ b/sampleSpecs/unknownFieldRetention.smithy @@ -2,12 +2,12 @@ $version: "2" namespace smithy4s.example -use alloy#unknownFieldRetention +use alloy#unknownDocumentFieldRetention structure UnknownFieldRetentionExample { foo: String bar: String - @unknownFieldRetention + @unknownDocumentFieldRetention retainedUnknownFields: Document } @@ -15,7 +15,7 @@ structure DefaultUnknownFieldRetentionExample { foo: String bar: String @default - @unknownFieldRetention + @unknownDocumentFieldRetention retainedUnknownFields: Document } @@ -23,7 +23,7 @@ structure RequiredUnknownFieldRetentionExample { foo: String bar: String @required - @unknownFieldRetention + @unknownDocumentFieldRetention retainedUnknownFields: Document } @@ -32,6 +32,6 @@ structure DefaultRequiredUnknownFieldRetentionExample { bar: String @default @required - @unknownFieldRetention + @unknownDocumentFieldRetention retainedUnknownFields: Document } From 82295759e0d165d573a4630ac33d7ae9f1f07ea8 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Thu, 7 Mar 2024 11:41:33 +0000 Subject: [PATCH 05/17] Add initial JSON tests --- .../test/src/smithy4s/DocumentSpec.scala | 4 +- .../json/SchemaVisitorJCodecTests.scala | 93 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala index 4cda60aa4..95128540e 100644 --- a/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala @@ -20,10 +20,10 @@ import smithy.api.JsonName import smithy.api.Default import alloy.Discriminated import munit._ -import smithy4s.example.IntList -import smithy4s.example.OperationOutput import smithy4s.example.DefaultRequiredUnknownFieldRetentionExample import smithy4s.example.DefaultUnknownFieldRetentionExample +import smithy4s.example.IntList +import smithy4s.example.OperationOutput import smithy4s.example.RequiredUnknownFieldRetentionExample import smithy4s.example.UnknownFieldRetentionExample diff --git a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala index 1a21e3551..f4f0610ed 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, Right(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[UnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, Right(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[UnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, Right(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[UnknownFieldRetentionExample](json) + + expect.same(json, expectedJson) + expect.same(roundTripped, Right(defaultRequiredUnknownFieldRetentionExample)) + } + } From 6467c958d0271f5cded7f2d62dc64d19dc5e7ba3 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Thu, 7 Mar 2024 13:03:23 +0000 Subject: [PATCH 06/17] JSON encoder support --- ...RequiredUnknownFieldRetentionExample.scala | 2 +- .../DefaultUnknownFieldRetentionExample.scala | 2 +- ...RequiredUnknownFieldRetentionExample.scala | 2 +- .../UnknownFieldRetentionExample.scala | 2 +- .../smithy4s/json/JsoniterCodecCompiler.scala | 3 +- .../src/smithy4s/json/internals/JCodec.scala | 2 + .../json/internals/SchemaVisitorJCodec.scala | 129 +++++++++++------- .../json/SchemaVisitorJCodecTests.scala | 8 +- sampleSpecs/unknownFieldRetention.smithy | 5 + 9 files changed, 96 insertions(+), 59 deletions(-) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala index b640639ad..dccf8243d 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultRequiredUnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object DefaultRequiredUnknownFieldRetentionExample extends ShapeTag.Companion[De implicit val schema: Schema[DefaultRequiredUnknownFieldRetentionExample] = struct( string.optional[DefaultRequiredUnknownFieldRetentionExample]("foo", _.foo), string.optional[DefaultRequiredUnknownFieldRetentionExample]("bar", _.bar), - document.required[DefaultRequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), smithy.api.Default(smithy4s.Document.nullDoc)), + 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 index 35b869d54..1f4561e15 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DefaultUnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object DefaultUnknownFieldRetentionExample extends ShapeTag.Companion[DefaultUnk 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)), + 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/RequiredUnknownFieldRetentionExample.scala b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala index ec7d20b63..e091a84bd 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/RequiredUnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object RequiredUnknownFieldRetentionExample extends ShapeTag.Companion[RequiredU implicit val schema: Schema[RequiredUnknownFieldRetentionExample] = struct( string.optional[RequiredUnknownFieldRetentionExample]("foo", _.foo), string.optional[RequiredUnknownFieldRetentionExample]("bar", _.bar), - document.required[RequiredUnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention()), + 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 index 9591299d7..d03763136 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/UnknownFieldRetentionExample.scala @@ -22,6 +22,6 @@ object UnknownFieldRetentionExample extends ShapeTag.Companion[UnknownFieldReten implicit val schema: Schema[UnknownFieldRetentionExample] = struct( string.optional[UnknownFieldRetentionExample]("foo", _.foo), string.optional[UnknownFieldRetentionExample]("bar", _.bar), - document.optional[UnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention()), + document.optional[UnknownFieldRetentionExample]("retainedUnknownFields", _.retainedUnknownFields).addHints(alloy.UnknownDocumentFieldRetention(), alloy.UnknownJsonFieldRetention()), )(make).withId(id).addHints(hints) } 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..7e865888e 100644 --- a/modules/json/src/smithy4s/json/internals/JCodec.scala +++ b/modules/json/src/smithy4s/json/internals/JCodec.scala @@ -21,6 +21,7 @@ package internals import com.github.plokhotnyuk.jsoniter_scala.core._ /** + * // TODO: Update this * 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. @@ -36,6 +37,7 @@ private[internals] trait JCodec[A] extends JsonCodec[A] { def expecting: String /** + * // TODO: Update this * States whether this codec expects data * from the body of an http request (as opposed to * from headers, query params, etc). Used to prevent diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index e4528c37a..9491666e4 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,7 +37,6 @@ 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 private[smithy4s] class SchemaVisitorJCodec( @@ -47,7 +47,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] = @@ -1299,26 +1298,74 @@ private[smithy4s] class SchemaVisitorJCodec( ) } + trait UnknownFieldsEncoder[A] { + def apply(a: A): JsonWriter => Unit + } + + object UnknownFieldsEncoder + extends SchemaVisitor.Default[UnknownFieldsEncoder] { self => + + override def default[A]: UnknownFieldsEncoder[A] = _ => _ => () + + override def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ): UnknownFieldsEncoder[P] = document => + tag match { + case PDocument => + document match { + case Document.DObject(values) => + jsonWriter => values.foreach {case (key, value) => + jsonWriter.writeKey(key) + documentJCodec.encodeValue(value, jsonWriter) + } + + case _ => + _ => () + } + + case _ => _ => () + } + + override def option[A]( + schema: Schema[A] + ): UnknownFieldsEncoder[Option[A]] = { + val encoder = self(schema) + locally { + case Some(a) => encoder.apply(a) + case None => _ => () + } + } + } + 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 ( + field.hints + .has(UnknownJsonFieldRetention) + ) { + val unknownFieldsEncoder = UnknownFieldsEncoder(field.schema) + (z: Z, out: JsonWriter) => unknownFieldsEncoder(field.get(z))(out) + } else { + 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(a, out) + codec.encodeValue(field.get(z), out) + } else { (z: Z, out: JsonWriter) => + 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)] @@ -1349,21 +1396,13 @@ private[smithy4s] class SchemaVisitorJCodec( 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 = { + override def decodeValue(cursor: Cursor, in: JsonReader): Z = { val buffer = new util.HashMap[String, Any](handlers.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 ({ @@ -1375,37 +1414,27 @@ private[smithy4s] class SchemaVisitorJCodec( 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) - if (value == null) { - if (default == null) - cursor.requiredFieldError(jsonLabel, jsonLabel) - else default - } else value - } + val stage2 = new VectorBuilder[Any] + fields.foreach { case (f, jsonLabel, default) => + stage2 += { + val value = buffer.get(f.label) + if (value == null) { + if (default == null) + cursor.requiredFieldError(jsonLabel, jsonLabel) + else default + } else value } - const(stage2.result()) } + const(stage2.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 f4f0610ed..f0d637d88 100644 --- a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala +++ b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala @@ -664,7 +664,7 @@ class SchemaVisitorJCodecTests() extends FunSuite { ) val json = writeToString(unknownFieldRetentionExample) - val expectedJson = """{foo:"foo",bar:"bar",unknownField1:"unknownString1",unknownField2:"unknownString2"}""" + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" val roundTripped = readFromString[UnknownFieldRetentionExample](json) @@ -686,7 +686,7 @@ class SchemaVisitorJCodecTests() extends FunSuite { ) val json = writeToString(defaultUnknownFieldRetentionExample) - val expectedJson = """{foo:"foo",bar:"bar",unknownField1:"unknownString1",unknownField2:"unknownString2"}""" + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" val roundTripped = readFromString[UnknownFieldRetentionExample](json) @@ -708,7 +708,7 @@ class SchemaVisitorJCodecTests() extends FunSuite { ) val json = writeToString(requiredUnknownFieldRetentionExample) - val expectedJson = """{foo:"foo",bar:"bar",unknownField1:"unknownString1",unknownField2:"unknownString2"}""" + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" val roundTripped = readFromString[UnknownFieldRetentionExample](json) @@ -730,7 +730,7 @@ class SchemaVisitorJCodecTests() extends FunSuite { ) val json = writeToString(defaultRequiredUnknownFieldRetentionExample) - val expectedJson = """{foo:"foo",bar:"bar",unknownField1:"unknownString1",unknownField2:"unknownString2"}""" + val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" val roundTripped = readFromString[UnknownFieldRetentionExample](json) diff --git a/sampleSpecs/unknownFieldRetention.smithy b/sampleSpecs/unknownFieldRetention.smithy index 3634c330a..9cb33e82b 100644 --- a/sampleSpecs/unknownFieldRetention.smithy +++ b/sampleSpecs/unknownFieldRetention.smithy @@ -3,11 +3,13 @@ $version: "2" namespace smithy4s.example use alloy#unknownDocumentFieldRetention +use alloy#unknownJsonFieldRetention structure UnknownFieldRetentionExample { foo: String bar: String @unknownDocumentFieldRetention + @unknownJsonFieldRetention retainedUnknownFields: Document } @@ -16,6 +18,7 @@ structure DefaultUnknownFieldRetentionExample { bar: String @default @unknownDocumentFieldRetention + @unknownJsonFieldRetention retainedUnknownFields: Document } @@ -24,6 +27,7 @@ structure RequiredUnknownFieldRetentionExample { bar: String @required @unknownDocumentFieldRetention + @unknownJsonFieldRetention retainedUnknownFields: Document } @@ -33,5 +37,6 @@ structure DefaultRequiredUnknownFieldRetentionExample { @default @required @unknownDocumentFieldRetention + @unknownJsonFieldRetention retainedUnknownFields: Document } From 043042418f633cc6b3af660ecda111779f84af58 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Thu, 7 Mar 2024 17:48:11 +0000 Subject: [PATCH 07/17] Json decoder support --- .../DocumentDecoderSchemaVisitor.scala | 8 +- .../json/internals/SchemaVisitorJCodec.scala | 127 +++++++++++++----- .../json/SchemaVisitorJCodecTests.scala | 14 +- 3 files changed, 108 insertions(+), 41 deletions(-) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index ecd4a55f0..7411b19c5 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -515,16 +515,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/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 9491666e4..556bab6dd 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -38,6 +38,7 @@ import scala.collection.compat.immutable.ArraySeq import scala.collection.immutable.VectorBuilder import scala.collection.mutable.ListBuffer import scala.collection.immutable.ListMap +import scala.jdk.CollectionConverters._ private[smithy4s] class SchemaVisitorJCodec( maxArity: Int, @@ -936,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 @@ -950,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 = @@ -1067,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 @@ -1083,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 = @@ -1274,12 +1275,58 @@ 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 } + trait UnknownFieldsDecoder[A] { self => + def apply( + in: JsonReader, + unknownFields: util.HashMap[String, Document] + ): A + } + + object UnknownFieldsDecoder + extends SchemaVisitor.Default[UnknownFieldsDecoder] { self => + + override def default[A]: UnknownFieldsDecoder[A] = ( + in: JsonReader, + _: util.HashMap[String, Document] + ) => in.decodeError("Expected JSON document") + + override def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ): UnknownFieldsDecoder[P] = ( + in: JsonReader, + unknownFields: util.HashMap[String, Document] + ) => + tag match { + case PDocument => Document.DObject(unknownFields.asScala.toMap) + case _ => in.decodeError("Expected JSON document") + } + + override def option[A]( + schema: Schema[A] + ): UnknownFieldsDecoder[Option[A]] = { + val decoder = schema.compile(self) + ( + in: JsonReader, + unknownFields: util.HashMap[String, Document] + ) => + if (unknownFields.isEmpty) None + else Some(decoder(in, unknownFields)) + } + } + + // TODO: Remove/rename mmap? private type Handler = (Cursor, JsonReader, util.HashMap[String, Any]) => Unit private def fieldHandler[Z, A]( @@ -1316,10 +1363,11 @@ private[smithy4s] class SchemaVisitorJCodec( case PDocument => document match { case Document.DObject(values) => - jsonWriter => values.foreach {case (key, value) => - jsonWriter.writeKey(key) - documentJCodec.encodeValue(value, jsonWriter) - } + jsonWriter => + values.foreach { case (key, value) => + jsonWriter.writeKey(key) + documentJCodec.encodeValue(value, jsonWriter) + } case _ => _ => () @@ -1342,19 +1390,16 @@ private[smithy4s] class SchemaVisitorJCodec( private def fieldEncoder[Z, A]( field: Field[Z, A] ): (Z, JsonWriter) => Unit = - if ( - field.hints - .has(UnknownJsonFieldRetention) - ) { + if (isForUnknownFieldRetention(field)) { val unknownFieldsEncoder = UnknownFieldsEncoder(field.schema) (z: Z, out: JsonWriter) => unknownFieldsEncoder(field.get(z))(out) } else { val codec = apply(field.schema) - val jLabel = jsonLabel(field) + val jsonLabel = jsonLabelOrLabel(field) val writeLabel: JsonWriter => Unit = - if (jLabel.forall(JsonWriter.isNonEscapedAscii)) { - _.writeNonEscapedAsciiKey(jLabel) - } else _.writeKey(jLabel) + if (jsonLabel.forall(JsonWriter.isNonEscapedAscii)) { + _.writeNonEscapedAsciiKey(jsonLabel) + } else _.writeKey(jsonLabel) if (explicitDefaultsEncoding) { (z: Z, out: JsonWriter) => writeLabel(out) @@ -1371,10 +1416,10 @@ private[smithy4s] class SchemaVisitorJCodec( 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]( @@ -1386,10 +1431,14 @@ private[smithy4s] class SchemaVisitorJCodec( ): JCodec[Z] = new JCodec[Z] { + private[this] val retainUnknownFields = fields.exists { + case (field, _, _) => isForUnknownFieldRetention(field) + } + private[this] val handlers = new util.HashMap[String, Handler](fields.length << 1, 0.5f) { - fields.foreach { case (field, jLabel, _) => - put(jLabel, fieldHandler(field)) + fields.foreach { case (field, jsonLabel, _) => + put(jsonLabel, fieldHandler(field)) } } @@ -1401,13 +1450,26 @@ private[smithy4s] class SchemaVisitorJCodec( override def canBeKey = false override def decodeValue(cursor: Cursor, in: JsonReader): Z = { + val unknownFields = + if (retainUnknownFields) + new util.HashMap[String, Document](handlers.size << 1, 0.5f) + else null + def retainUnknownField(key: String): Unit = { + val value = documentJCodec.decodeValue(cursor, in) + unknownFields.put(key, value) + () + } val buffer = new util.HashMap[String, Any](handlers.size << 1, 0.5f) if (in.isNextToken('{')) { if (!in.isNextToken('}')) { in.rollbackToken() while ({ - val handler = handlers.get(in.readKeyAsString()) - if (handler eq null) in.skip() + val key = in.readKeyAsString() + val handler = handlers.get(key) + if (handler eq null) + if (retainUnknownFields) + retainUnknownField(key) + else in.skip() else handler(cursor, in, buffer) in.isNextToken(',') }) () @@ -1415,14 +1477,19 @@ private[smithy4s] class SchemaVisitorJCodec( } } else in.decodeError("Expected JSON object") val stage2 = new VectorBuilder[Any] - fields.foreach { case (f, jsonLabel, default) => + fields.foreach { case (field, jsonLabel, default) => stage2 += { - val value = buffer.get(f.label) - if (value == null) { - if (default == null) - cursor.requiredFieldError(jsonLabel, jsonLabel) - else default - } else value + if (isForUnknownFieldRetention(field)) { + val unknownFieldsDecoder = UnknownFieldsDecoder(field.schema) + unknownFieldsDecoder(in, unknownFields) + } else { + val value = buffer.get(field.label) + if (value == null) { + if (default == null) + cursor.requiredFieldError(jsonLabel, jsonLabel) + else default + } else value + } } } const(stage2.result()) diff --git a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala index f0d637d88..1d4fe27f5 100644 --- a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala +++ b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala @@ -669,7 +669,7 @@ class SchemaVisitorJCodecTests() extends FunSuite { val roundTripped = readFromString[UnknownFieldRetentionExample](json) expect.same(json, expectedJson) - expect.same(roundTripped, Right(unknownFieldRetentionExample)) + expect.same(roundTripped, unknownFieldRetentionExample) } test( @@ -688,10 +688,10 @@ class SchemaVisitorJCodecTests() extends FunSuite { val json = writeToString(defaultUnknownFieldRetentionExample) val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" - val roundTripped = readFromString[UnknownFieldRetentionExample](json) + val roundTripped = readFromString[DefaultUnknownFieldRetentionExample](json) expect.same(json, expectedJson) - expect.same(roundTripped, Right(defaultUnknownFieldRetentionExample)) + expect.same(roundTripped, defaultUnknownFieldRetentionExample) } test( @@ -710,10 +710,10 @@ class SchemaVisitorJCodecTests() extends FunSuite { val json = writeToString(requiredUnknownFieldRetentionExample) val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" - val roundTripped = readFromString[UnknownFieldRetentionExample](json) + val roundTripped = readFromString[RequiredUnknownFieldRetentionExample](json) expect.same(json, expectedJson) - expect.same(roundTripped, Right(requiredUnknownFieldRetentionExample)) + expect.same(roundTripped, requiredUnknownFieldRetentionExample) } test( @@ -732,10 +732,10 @@ class SchemaVisitorJCodecTests() extends FunSuite { val json = writeToString(defaultRequiredUnknownFieldRetentionExample) val expectedJson = """{"foo":"foo","bar":"bar","unknownField1":"unknownString1","unknownField2":"unknownString2"}""" - val roundTripped = readFromString[UnknownFieldRetentionExample](json) + val roundTripped = readFromString[DefaultRequiredUnknownFieldRetentionExample](json) expect.same(json, expectedJson) - expect.same(roundTripped, Right(defaultRequiredUnknownFieldRetentionExample)) + expect.same(roundTripped, defaultRequiredUnknownFieldRetentionExample) } } From 4f07a625cd116f842621070c30914cebc9da98f6 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 11:55:57 +0000 Subject: [PATCH 08/17] Remove redundant code --- .../src/smithy4s/json/internals/JCodec.scala | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/modules/json/src/smithy4s/json/internals/JCodec.scala b/modules/json/src/smithy4s/json/internals/JCodec.scala index 7e865888e..7e3667567 100644 --- a/modules/json/src/smithy4s/json/internals/JCodec.scala +++ b/modules/json/src/smithy4s/json/internals/JCodec.scala @@ -20,15 +20,6 @@ package internals import com.github.plokhotnyuk.jsoniter_scala.core._ -/** - * // TODO: Update this - * 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 => @@ -36,15 +27,6 @@ private[internals] trait JCodec[A] extends JsonCodec[A] { def expecting: String - /** - * // TODO: Update this - * 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 = From b3a6d2a8e2e94f710006539da7fc605a719e0ea6 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 12:03:51 +0000 Subject: [PATCH 09/17] Handle naming TODO --- .../json/internals/SchemaVisitorJCodec.scala | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 556bab6dd..ff3b9ebcc 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -1326,7 +1326,6 @@ private[smithy4s] class SchemaVisitorJCodec( } } - // TODO: Remove/rename mmap? private type Handler = (Cursor, JsonReader, util.HashMap[String, Any]) => Unit private def fieldHandler[Z, A]( @@ -1334,8 +1333,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) @@ -1454,12 +1453,8 @@ private[smithy4s] class SchemaVisitorJCodec( if (retainUnknownFields) new util.HashMap[String, Document](handlers.size << 1, 0.5f) else null - def retainUnknownField(key: String): Unit = { - val value = documentJCodec.decodeValue(cursor, in) - unknownFields.put(key, value) - () - } - val buffer = new util.HashMap[String, Any](handlers.size << 1, 0.5f) + val knownFields = + new util.HashMap[String, Any](handlers.size << 1, 0.5f) if (in.isNextToken('{')) { if (!in.isNextToken('}')) { in.rollbackToken() @@ -1467,23 +1462,24 @@ private[smithy4s] class SchemaVisitorJCodec( val key = in.readKeyAsString() val handler = handlers.get(key) if (handler eq null) - if (retainUnknownFields) - retainUnknownField(key) - else in.skip() - else handler(cursor, in, buffer) + if (retainUnknownFields) { + val value = documentJCodec.decodeValue(cursor, in) + unknownFields.put(key, value) + } else in.skip() + else handler(cursor, in, knownFields) in.isNextToken(',') }) () if (!in.isCurrentToken('}')) in.objectEndOrCommaError() } } else in.decodeError("Expected JSON object") - val stage2 = new VectorBuilder[Any] + val values = new VectorBuilder[Any] fields.foreach { case (field, jsonLabel, default) => - stage2 += { + values += { if (isForUnknownFieldRetention(field)) { val unknownFieldsDecoder = UnknownFieldsDecoder(field.schema) unknownFieldsDecoder(in, unknownFields) } else { - val value = buffer.get(field.label) + val value = knownFields.get(field.label) if (value == null) { if (default == null) cursor.requiredFieldError(jsonLabel, jsonLabel) @@ -1492,7 +1488,7 @@ private[smithy4s] class SchemaVisitorJCodec( } } } - const(stage2.result()) + const(values.result()) } override def encodeValue(z: Z, out: JsonWriter): Unit = From 10e598553a373848ce39ca7efc10b65f7418544b Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 12:13:24 +0000 Subject: [PATCH 10/17] Regenerate bootstrap --- .../src/generated/smithy4s/example/Integers.scala | 8 ++++---- .../src/generated/smithy4s/example/Longs.scala | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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) } From 26955435db9445605b58488ec1c1187cea6fbb9f Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 12:24:47 +0000 Subject: [PATCH 11/17] Remove redundant types --- .../internals/DocumentDecoderSchemaVisitor.scala | 15 +++------------ .../json/internals/SchemaVisitorJCodec.scala | 16 ++++------------ 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index 7411b19c5..474d80c7f 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -318,10 +318,7 @@ class DocumentDecoderSchemaVisitor( object UnknownFieldsDecoder extends SchemaVisitor.Default[UnknownFieldsDecoder] { self => - override def default[A]: UnknownFieldsDecoder[A] = ( - history: List[PayloadPath.Segment], - _: Map[String, Document] - ) => + override def default[A]: UnknownFieldsDecoder[A] = (history, _) => throw PayloadError( PayloadPath(history.reverse), "Json document", @@ -332,10 +329,7 @@ class DocumentDecoderSchemaVisitor( shapeId: ShapeId, hints: Hints, tag: Primitive[P] - ): UnknownFieldsDecoder[P] = ( - history: List[PayloadPath.Segment], - unknownFields: Map[String, Document] - ) => + ): UnknownFieldsDecoder[P] = (history, unknownFields) => tag match { case PDocument => Document.DObject(unknownFields) case _ => @@ -350,10 +344,7 @@ class DocumentDecoderSchemaVisitor( schema: Schema[A] ): UnknownFieldsDecoder[Option[A]] = { val decoder = schema.compile(self) - ( - history: List[PayloadPath.Segment], - unknownFields: Map[String, Document] - ) => + (history, unknownFields) => if (unknownFields.isEmpty) None else Some(decoder(history, unknownFields)) } diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index ff3b9ebcc..893a3470d 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -1295,19 +1295,14 @@ private[smithy4s] class SchemaVisitorJCodec( object UnknownFieldsDecoder extends SchemaVisitor.Default[UnknownFieldsDecoder] { self => - override def default[A]: UnknownFieldsDecoder[A] = ( - in: JsonReader, - _: util.HashMap[String, Document] - ) => in.decodeError("Expected JSON document") + override def default[A]: UnknownFieldsDecoder[A] = (in, _) => + in.decodeError("Expected JSON document") override def primitive[P]( shapeId: ShapeId, hints: Hints, tag: Primitive[P] - ): UnknownFieldsDecoder[P] = ( - in: JsonReader, - unknownFields: util.HashMap[String, Document] - ) => + ): UnknownFieldsDecoder[P] = (in, unknownFields) => tag match { case PDocument => Document.DObject(unknownFields.asScala.toMap) case _ => in.decodeError("Expected JSON document") @@ -1317,10 +1312,7 @@ private[smithy4s] class SchemaVisitorJCodec( schema: Schema[A] ): UnknownFieldsDecoder[Option[A]] = { val decoder = schema.compile(self) - ( - in: JsonReader, - unknownFields: util.HashMap[String, Document] - ) => + (in, unknownFields) => if (unknownFields.isEmpty) None else Some(decoder(in, unknownFields)) } From 59e0d225da28823f1679bc8b7243e0deab05a75a Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 15:31:59 +0000 Subject: [PATCH 12/17] Remove redundant schema visitors --- .../DocumentDecoderSchemaVisitor.scala | 57 ++------- .../DocumentEncoderSchemaVisitor.scala | 45 ++----- .../json/internals/SchemaVisitorJCodec.scala | 116 +++++------------- 3 files changed, 50 insertions(+), 168 deletions(-) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index 474d80c7f..78d9e2f9b 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -308,48 +308,6 @@ class DocumentDecoderSchemaVisitor( } } - trait UnknownFieldsDecoder[A] { self => - def apply( - history: List[PayloadPath.Segment], - unknownFields: Map[String, Document] - ): A - } - - object UnknownFieldsDecoder - extends SchemaVisitor.Default[UnknownFieldsDecoder] { self => - - override def default[A]: UnknownFieldsDecoder[A] = (history, _) => - throw PayloadError( - PayloadPath(history.reverse), - "Json document", - "Expected Json Shape: Object" - ) - - override def primitive[P]( - shapeId: ShapeId, - hints: Hints, - tag: Primitive[P] - ): UnknownFieldsDecoder[P] = (history, unknownFields) => - tag match { - case PDocument => Document.DObject(unknownFields) - case _ => - throw PayloadError( - PayloadPath(history.reverse), - "Json document", - "Expected Json Shape: Object" - ) - } - - override def option[A]( - schema: Schema[A] - ): UnknownFieldsDecoder[Option[A]] = { - val decoder = schema.compile(self) - (history, unknownFields) => - if (unknownFields.isEmpty) None - else Some(decoder(history, unknownFields)) - } - } - override def struct[S]( shapeId: ShapeId, hints: Hints, @@ -368,14 +326,25 @@ class DocumentDecoderSchemaVisitor( Map[String, Document] ) => Unit = if (isForUnknownFieldRetention(field)) { - val unknownFieldsDecoder = UnknownFieldsDecoder(field.schema) + // TODO: Lift out. + val unknownFieldsDecoder = Document.Decoder.fromSchema(field.schema) ( pp: List[PayloadPath.Segment], buffer: Any => Unit, fields: Map[String, Document] ) => { val unknownFields = fields -- knownFieldLabels - buffer(unknownFieldsDecoder(pp, unknownFields)) + buffer( + unknownFieldsDecoder + .decode(Document.DObject(unknownFields)) + .getOrElse( + throw new PayloadError( + PayloadPath(pp.reverse), + "Json document", + "Expected Json Shape: Object" + ) + ) + ) } } else { val jsonLabel = jsonLabelOrLabel(field) diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index 68bc3055f..d945608d4 100644 --- a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala @@ -182,42 +182,6 @@ class DocumentEncoderSchemaVisitor( from(e => DString(total(e).stringValue)) } - trait UnknownFieldsEncoder[A] { - def apply(a: A): Map[String, Document] - } - - object UnknownFieldsEncoder - extends SchemaVisitor.Default[UnknownFieldsEncoder] { self => - - override def default[A]: UnknownFieldsEncoder[A] = _ => Map.empty - - override def primitive[P]( - shapeId: ShapeId, - hints: Hints, - tag: Primitive[P] - ): UnknownFieldsEncoder[P] = document => - tag match { - case PDocument => - document match { - case Document.DObject(values) => values - case _ => Map.empty - } - - case _ => - Map.empty - } - - override def option[A]( - schema: Schema[A] - ): UnknownFieldsEncoder[Option[A]] = { - val encoder = self(schema) - locally { - case Some(a) => encoder.apply(a) - case None => Map.empty - } - } - } - override def struct[S]( shapeId: ShapeId, hints: Hints, @@ -237,8 +201,13 @@ class DocumentEncoderSchemaVisitor( field.hints .has(UnknownDocumentFieldRetention) ) { - val unknownFieldsEncoder = UnknownFieldsEncoder(field.schema) - (s, builder) => builder ++= unknownFieldsEncoder(field.get(s)) + // TODO: Lift out. + val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) + (s, builder) => + unknownFieldsEncoder.encode(field.get(s)) match { + case Document.DObject(values) => builder ++= values + case _ => () + } } else { val encoder = apply(field.schema) val jsonLabel = field.hints diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 893a3470d..8a349bdf6 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -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" ) @@ -1285,39 +1285,6 @@ private[smithy4s] class SchemaVisitorJCodec( case Some(x) => x.value } - trait UnknownFieldsDecoder[A] { self => - def apply( - in: JsonReader, - unknownFields: util.HashMap[String, Document] - ): A - } - - object UnknownFieldsDecoder - extends SchemaVisitor.Default[UnknownFieldsDecoder] { self => - - override def default[A]: UnknownFieldsDecoder[A] = (in, _) => - in.decodeError("Expected JSON document") - - override def primitive[P]( - shapeId: ShapeId, - hints: Hints, - tag: Primitive[P] - ): UnknownFieldsDecoder[P] = (in, unknownFields) => - tag match { - case PDocument => Document.DObject(unknownFields.asScala.toMap) - case _ => in.decodeError("Expected JSON document") - } - - override def option[A]( - schema: Schema[A] - ): UnknownFieldsDecoder[Option[A]] = { - val decoder = schema.compile(self) - (in, unknownFields) => - if (unknownFields.isEmpty) None - else Some(decoder(in, unknownFields)) - } - } - private type Handler = (Cursor, JsonReader, util.HashMap[String, Any]) => Unit private def fieldHandler[Z, A]( @@ -1336,54 +1303,22 @@ private[smithy4s] class SchemaVisitorJCodec( ) } - trait UnknownFieldsEncoder[A] { - def apply(a: A): JsonWriter => Unit - } - - object UnknownFieldsEncoder - extends SchemaVisitor.Default[UnknownFieldsEncoder] { self => - - override def default[A]: UnknownFieldsEncoder[A] = _ => _ => () - - override def primitive[P]( - shapeId: ShapeId, - hints: Hints, - tag: Primitive[P] - ): UnknownFieldsEncoder[P] = document => - tag match { - case PDocument => - document match { - case Document.DObject(values) => - jsonWriter => - values.foreach { case (key, value) => - jsonWriter.writeKey(key) - documentJCodec.encodeValue(value, jsonWriter) - } - - case _ => - _ => () - } - - case _ => _ => () - } - - override def option[A]( - schema: Schema[A] - ): UnknownFieldsEncoder[Option[A]] = { - val encoder = self(schema) - locally { - case Some(a) => encoder.apply(a) - case None => _ => () - } - } - } - private def fieldEncoder[Z, A]( field: Field[Z, A] ): (Z, JsonWriter) => Unit = if (isForUnknownFieldRetention(field)) { - val unknownFieldsEncoder = UnknownFieldsEncoder(field.schema) - (z: Z, out: JsonWriter) => unknownFieldsEncoder(field.get(z))(out) + val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) + (z: Z, out: JsonWriter) => + 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) @@ -1468,8 +1403,17 @@ private[smithy4s] class SchemaVisitorJCodec( fields.foreach { case (field, jsonLabel, default) => values += { if (isForUnknownFieldRetention(field)) { - val unknownFieldsDecoder = UnknownFieldsDecoder(field.schema) - unknownFieldsDecoder(in, unknownFields) + // TODO: Lift out. + val unknownFieldsDecoder = + Document.Decoder.fromSchema(field.schema) + unknownFieldsDecoder + .decode(Document.DObject(unknownFields.asScala.toMap)) + .getOrElse( + cursor.payloadError( + this, + "Expected JSON document" + ) + ) } else { val value = knownFields.get(field.label) if (value == null) { From b7dbe274af1b0284096d1710d44be3420272af1e Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 15:39:56 +0000 Subject: [PATCH 13/17] Remove redundant TODOs --- .../src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala | 1 - .../src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala | 1 - 2 files changed, 2 deletions(-) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index 78d9e2f9b..a095522ec 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -326,7 +326,6 @@ class DocumentDecoderSchemaVisitor( Map[String, Document] ) => Unit = if (isForUnknownFieldRetention(field)) { - // TODO: Lift out. val unknownFieldsDecoder = Document.Decoder.fromSchema(field.schema) ( pp: List[PayloadPath.Segment], diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index d945608d4..9e6d6041a 100644 --- a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala @@ -201,7 +201,6 @@ class DocumentEncoderSchemaVisitor( field.hints .has(UnknownDocumentFieldRetention) ) { - // TODO: Lift out. val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) (s, builder) => unknownFieldsEncoder.encode(field.get(s)) match { From 50038a6a778e836d83433ed67a7c3e0c849b52a7 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 15:47:44 +0000 Subject: [PATCH 14/17] Remove redunant types --- .../DocumentDecoderSchemaVisitor.scala | 31 +++++-------------- .../DocumentEncoderSchemaVisitor.scala | 5 +-- .../json/internals/SchemaVisitorJCodec.scala | 21 +++++++------ 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala index a095522ec..7377955de 100644 --- a/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentDecoderSchemaVisitor.scala @@ -314,8 +314,8 @@ class DocumentDecoderSchemaVisitor( fields: Vector[Field[S, _]], make: IndexedSeq[Any] => S ): DocumentDecoder[S] = { - def isForUnknownFieldRetention(field: Field[S, _]): Boolean = field.hints - .has(UnknownDocumentFieldRetention) + 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) val knownFieldLabels = @@ -327,11 +327,7 @@ class DocumentDecoderSchemaVisitor( ) => Unit = if (isForUnknownFieldRetention(field)) { val unknownFieldsDecoder = Document.Decoder.fromSchema(field.schema) - ( - pp: List[PayloadPath.Segment], - buffer: Any => Unit, - fields: Map[String, Document] - ) => { + (pp, buffer, fields) => val unknownFields = fields -- knownFieldLabels buffer( unknownFieldsDecoder @@ -344,35 +340,23 @@ class DocumentDecoderSchemaVisitor( ) ) ) - } } else { val jsonLabel = jsonLabelOrLabel(field) field.getDefaultValue match { case Some(defaultValue) => - ( - pp: List[PayloadPath.Segment], - buffer: Any => Unit, - fields: Map[String, Document] - ) => { + (pp, buffer, fields) => val path = PayloadPath.Segment(jsonLabel) :: pp - fields - .get(jsonLabel) match { + fields.get(jsonLabel) 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] - ) => { + (pp, buffer, fields) => val path = PayloadPath.Segment(jsonLabel) :: pp - fields - .get(jsonLabel) match { + fields.get(jsonLabel) match { case Some(document) => buffer(apply(field.schema)(path, document)) case None => @@ -382,7 +366,6 @@ class DocumentDecoderSchemaVisitor( "Required field not found" ) } - } } } val fieldDecoders = fields.map(field => fieldDecoder(field)) diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala index 9e6d6041a..4f3674953 100644 --- a/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/internals/DocumentEncoderSchemaVisitor.scala @@ -197,10 +197,7 @@ class DocumentEncoderSchemaVisitor( def fieldEncoder[A]( field: Field[S, A] ): (S, Builder[(String, Document), Map[String, Document]]) => Unit = - if ( - field.hints - .has(UnknownDocumentFieldRetention) - ) { + if (field.hints.has(UnknownDocumentFieldRetention)) { val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) (s, builder) => unknownFieldsEncoder.encode(field.get(s)) match { diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 8a349bdf6..197633cca 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -1308,7 +1308,7 @@ private[smithy4s] class SchemaVisitorJCodec( ): (Z, JsonWriter) => Unit = if (isForUnknownFieldRetention(field)) { val unknownFieldsEncoder = Document.Encoder.fromSchema(field.schema) - (z: Z, out: JsonWriter) => + (z, out) => unknownFieldsEncoder.encode(field.get(z)) match { case Document.DObject(values) => values.foreach { case (key, value) => @@ -1327,14 +1327,16 @@ private[smithy4s] class SchemaVisitorJCodec( _.writeNonEscapedAsciiKey(jsonLabel) } else _.writeKey(jsonLabel) - if (explicitDefaultsEncoding) { (z: Z, out: JsonWriter) => - writeLabel(out) - codec.encodeValue(field.get(z), out) - } else { (z: Z, out: JsonWriter) => - field.foreachUnlessDefault(z) { (a: A) => + 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) + } } } @@ -1364,7 +1366,8 @@ private[smithy4s] class SchemaVisitorJCodec( private[this] val handlers = new util.HashMap[String, Handler](fields.length << 1, 0.5f) { fields.foreach { case (field, jsonLabel, _) => - put(jsonLabel, fieldHandler(field)) + if (!isForUnknownFieldRetention(field)) + put(jsonLabel, fieldHandler(field)) } } From a42616fc96812808dadb90bf5cb7d938ed4a6050 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 18:02:20 +0000 Subject: [PATCH 15/17] wip --- .../json/internals/SchemaVisitorJCodec.scala | 82 +++++++++++-------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 197633cca..44c49dc59 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -1327,16 +1327,14 @@ private[smithy4s] class SchemaVisitorJCodec( _.writeNonEscapedAsciiKey(jsonLabel) } else _.writeKey(jsonLabel) - if (explicitDefaultsEncoding) { - (z, out) => + if (explicitDefaultsEncoding) { (z, out) => + writeLabel(out) + codec.encodeValue(field.get(z), out) + } else { (z, out) => + field.foreachUnlessDefault(z) { (a: A) => writeLabel(out) - codec.encodeValue(field.get(z), out) - } else { - (z, out) => - field.foreachUnlessDefault(z) { (a: A) => - writeLabel(out) - codec.encodeValue(a, out) - } + codec.encodeValue(a, out) + } } } @@ -1359,18 +1357,33 @@ private[smithy4s] class SchemaVisitorJCodec( ): JCodec[Z] = new JCodec[Z] { - private[this] val retainUnknownFields = fields.exists { - case (field, _, _) => isForUnknownFieldRetention(field) + private[this] val ( + knownFields: LabelledFields[Z], + unknownFieldRetainers: LabelledFields[Z] + ) = fields.partition { case (field, _, _) => + isForUnknownFieldRetention(field) } - private[this] val handlers = - new util.HashMap[String, Handler](fields.length << 1, 0.5f) { - fields.foreach { case (field, jsonLabel, _) => - if (!isForUnknownFieldRetention(field)) - put(jsonLabel, fieldHandler(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, fieldHandler(field)) } } + private[this] val retainUnknownFields = + !unknownFieldRetainerHandlers.isEmpty + private[this] val documentEncoders = fields.map(labelledField => fieldEncoder(labelledField._1)) @@ -1379,24 +1392,23 @@ private[smithy4s] class SchemaVisitorJCodec( override def canBeKey = false override def decodeValue(cursor: Cursor, in: JsonReader): Z = { - val unknownFields = - if (retainUnknownFields) - new util.HashMap[String, Document](handlers.size << 1, 0.5f) + val unknownFieldValues = + if (retainUnknownFields) new util.HashMap[String, Document] else null - val knownFields = - new util.HashMap[String, Any](handlers.size << 1, 0.5f) + val knownFieldValues = + new util.HashMap[String, Any](knownFieldsHandlers.size << 1, 0.5f) if (in.isNextToken('{')) { if (!in.isNextToken('}')) { in.rollbackToken() while ({ val key = in.readKeyAsString() - val handler = handlers.get(key) + val handler = knownFieldsHandlers.get(key) if (handler eq null) if (retainUnknownFields) { val value = documentJCodec.decodeValue(cursor, in) - unknownFields.put(key, value) + unknownFieldValues.put(key, value) } else in.skip() - else handler(cursor, in, knownFields) + else handler(cursor, in, knownFieldValues) in.isNextToken(',') }) () if (!in.isCurrentToken('}')) in.objectEndOrCommaError() @@ -1404,26 +1416,28 @@ private[smithy4s] class SchemaVisitorJCodec( } else in.decodeError("Expected JSON object") val values = new VectorBuilder[Any] fields.foreach { case (field, jsonLabel, default) => - values += { - if (isForUnknownFieldRetention(field)) { + 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(unknownFields.asScala.toMap)) + .decode(Document.DObject(unknownFieldValues.asScala.toMap)) .getOrElse( cursor.payloadError( this, "Expected JSON document" ) ) - } else { - val value = knownFields.get(field.label) - if (value == null) { - if (default == null) - cursor.requiredFieldError(jsonLabel, jsonLabel) - else default - } else value } } } From 6bdf6812246d57885922eaa0388fbcb918c3dac9 Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 18:03:38 +0000 Subject: [PATCH 16/17] Prep for avoiding reallocating document decoder on each decode --- .../json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 44c49dc59..1f14a5d61 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -1361,7 +1361,7 @@ private[smithy4s] class SchemaVisitorJCodec( knownFields: LabelledFields[Z], unknownFieldRetainers: LabelledFields[Z] ) = fields.partition { case (field, _, _) => - isForUnknownFieldRetention(field) + !isForUnknownFieldRetention(field) } private[this] val knownFieldsHandlers = From 521d5341322d6055906a8e4f6486b7185cfe565f Mon Sep 17 00:00:00 2001 From: David Piggott Date: Mon, 11 Mar 2024 18:05:21 +0000 Subject: [PATCH 17/17] Add TODO --- .../src/smithy4s/json/internals/SchemaVisitorJCodec.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index 1f14a5d61..773aa9989 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -1377,7 +1377,9 @@ private[smithy4s] class SchemaVisitorJCodec( 0.5f ) { unknownFieldRetainers.foreach { case (field, _, _) => - put(field.label, fieldHandler(field)) + put(field.label, + // TODO: Change this to solve the TODO further down + fieldHandler(field)) } }