diff --git a/CHANGELOG.md b/CHANGELOG.md index 0735f6fc5..a3909041d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Thank you! # 0.18.25 +* Add A flag to allow for numerics to be decoded from JSON strings (in smithy4s-json). +* Fixes issues in which applications of some Smithy traits would be incorrectly rendered in Scala code (see [#1602](https://github.com/disneystreaming/smithy4s/pull/1602)). * Fixes an issue in which refinements wouldn't work on custom simple shapes (newtypes) (see [#1595](https://github.com/disneystreaming/smithy4s/pull/1595)) * Fixes a regression from 0.18.4 which incorrectly rendered default values for certain types (see [#1593](https://github.com/disneystreaming/smithy4s/pull/1593)) * Fixes an issue in which union members targetting Unit would fail to compile when used as traits (see [#1600](https://github.com/disneystreaming/smithy4s/pull/1600)). diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DeprecatedUnion.scala b/modules/bootstrapped/src/generated/smithy4s/example/DeprecatedUnion.scala index 4e8080a68..aa3006fd3 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/DeprecatedUnion.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/DeprecatedUnion.scala @@ -51,7 +51,7 @@ object DeprecatedUnion extends ShapeTag.Companion[DeprecatedUnion] { def $ordinal: Int = 2 } - object DeprecatedUnionProductCase extends ShapeTag.Companion[DeprecatedUnionProductCase] { + object DeprecatedUnionProductCase { val id: ShapeId = ShapeId("smithy4s.example", "DeprecatedUnionProductCase") val hints: Hints = Hints( @@ -59,7 +59,7 @@ object DeprecatedUnion extends ShapeTag.Companion[DeprecatedUnion] { ).lazily - implicit val schema: Schema[DeprecatedUnionProductCase] = constant(DeprecatedUnionProductCase()).withId(id).addHints(hints) + val schema: Schema[DeprecatedUnionProductCase] = constant(DeprecatedUnionProductCase()).withId(id).addHints(hints) val alt = schema.oneOf[DeprecatedUnion]("p") } @@ -68,7 +68,7 @@ object DeprecatedUnion extends ShapeTag.Companion[DeprecatedUnion] { def $ordinal: Int = 3 } - object UnionProductCaseDeprecatedAtCallSite extends ShapeTag.Companion[UnionProductCaseDeprecatedAtCallSite] { + object UnionProductCaseDeprecatedAtCallSite { val id: ShapeId = ShapeId("smithy4s.example", "UnionProductCaseDeprecatedAtCallSite") val hints: Hints = Hints( @@ -76,7 +76,7 @@ object DeprecatedUnion extends ShapeTag.Companion[DeprecatedUnion] { ).lazily - implicit val schema: Schema[UnionProductCaseDeprecatedAtCallSite] = constant(UnionProductCaseDeprecatedAtCallSite()).withId(id).addHints(hints) + val schema: Schema[UnionProductCaseDeprecatedAtCallSite] = constant(UnionProductCaseDeprecatedAtCallSite()).withId(id).addHints(hints) val alt = schema.oneOf[DeprecatedUnion]("p2") } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/OrderType.scala b/modules/bootstrapped/src/generated/smithy4s/example/OrderType.scala index 5a2dcdd22..ec7071bbf 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/OrderType.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/OrderType.scala @@ -48,7 +48,7 @@ object OrderType extends ShapeTag.Companion[OrderType] { def $ordinal: Int = 1 } - object InStoreOrder extends ShapeTag.Companion[InStoreOrder] { + object InStoreOrder { val id: ShapeId = ShapeId("smithy4s.example", "InStoreOrder") val hints: Hints = Hints( diff --git a/modules/bootstrapped/src/generated/smithy4s/example/PersonUnion.scala b/modules/bootstrapped/src/generated/smithy4s/example/PersonUnion.scala index 830ce2d56..5e3e3b402 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/PersonUnion.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/PersonUnion.scala @@ -32,7 +32,7 @@ object PersonUnion extends ShapeTag.Companion[PersonUnion] { def $ordinal: Int = 0 } - object OtherPerson extends ShapeTag.Companion[OtherPerson] { + object OtherPerson { val id: ShapeId = ShapeId("smithy4s.example", "OtherPerson") val hints: Hints = Hints.empty diff --git a/modules/bootstrapped/src/generated/smithy4s/example/Podcast.scala b/modules/bootstrapped/src/generated/smithy4s/example/Podcast.scala index f5a10d5a3..08baea638 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/Podcast.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/Podcast.scala @@ -43,7 +43,7 @@ object Podcast extends ShapeTag.Companion[Podcast] { def $ordinal: Int = 0 } - object Video extends ShapeTag.Companion[Video] { + object Video { val id: ShapeId = ShapeId("smithy4s.example", "Video") val hints: Hints = Hints.empty @@ -69,7 +69,7 @@ object Podcast extends ShapeTag.Companion[Podcast] { def $ordinal: Int = 1 } - object Audio extends ShapeTag.Companion[Audio] { + object Audio { val id: ShapeId = ShapeId("smithy4s.example", "Audio") val hints: Hints = Hints.empty diff --git a/modules/bootstrapped/src/generated/smithy4s/example/TestAdt.scala b/modules/bootstrapped/src/generated/smithy4s/example/TestAdt.scala index 42b173c7a..d6287ac67 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/TestAdt.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/TestAdt.scala @@ -40,7 +40,7 @@ object TestAdt extends ShapeTag.Companion[TestAdt] { def $ordinal: Int = 0 } - object AdtOne extends ShapeTag.Companion[AdtOne] { + object AdtOne { val id: ShapeId = ShapeId("smithy4s.example", "AdtOne") val hints: Hints = Hints.empty @@ -61,7 +61,7 @@ object TestAdt extends ShapeTag.Companion[TestAdt] { def $ordinal: Int = 1 } - object AdtTwo extends ShapeTag.Companion[AdtTwo] { + object AdtTwo { val id: ShapeId = ShapeId("smithy4s.example", "AdtTwo") val hints: Hints = Hints.empty diff --git a/modules/bootstrapped/src/generated/smithy4s/example/TestMixinAdt.scala b/modules/bootstrapped/src/generated/smithy4s/example/TestMixinAdt.scala index 3bcbf9854..aab179c86 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/TestMixinAdt.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/TestMixinAdt.scala @@ -33,7 +33,7 @@ object TestMixinAdt extends ShapeTag.Companion[TestMixinAdt] { def $ordinal: Int = 0 } - object TestAdtMemberWithMixin extends ShapeTag.Companion[TestAdtMemberWithMixin] { + object TestAdtMemberWithMixin { val id: ShapeId = ShapeId("smithy4s.example", "TestAdtMemberWithMixin") val hints: Hints = Hints.empty diff --git a/modules/bootstrapped/src/generated/smithy4s/example/TestString.scala b/modules/bootstrapped/src/generated/smithy4s/example/TestString.scala index 152c04113..b9c3a417b 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/TestString.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/TestString.scala @@ -10,7 +10,7 @@ import smithy4s.schema.Schema.string object TestString extends Newtype[String] { val id: ShapeId = ShapeId("smithy4s.example", "TestString") val hints: Hints = Hints( - smithy4s.example.TestTrait(orderType = Some(smithy4s.example.OrderType.InStoreOrder(id = smithy4s.example.OrderNumber(100), locationId = Some("someLocation")))), + smithy4s.example.TestTrait(orderType = Some(smithy4s.example.OrderType.InStoreOrder(id = smithy4s.example.OrderNumber(100), locationId = Some("someLocation")).widen)), ).lazily val underlyingSchema: Schema[String] = string.withId(id).addHints(hints) implicit val schema: Schema[TestString] = bijection(underlyingSchema, asBijection) diff --git a/modules/bootstrapped/src/generated/smithy4s/example/_package/MyPackageStringTrait.scala b/modules/bootstrapped/src/generated/smithy4s/example/_package/MyPackageStringTrait.scala new file mode 100644 index 000000000..b715f4b33 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/_package/MyPackageStringTrait.scala @@ -0,0 +1,18 @@ +package smithy4s.example._package + +import smithy4s.Hints +import smithy4s.Newtype +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.schema.Schema.bijection +import smithy4s.schema.Schema.recursive +import smithy4s.schema.Schema.string + +object MyPackageStringTrait extends Newtype[String] { + val id: ShapeId = ShapeId("smithy4s.example.package", "MyPackageStringTrait") + val hints: Hints = Hints( + smithy.api.Trait(selector = None, structurallyExclusive = None, conflicts = None, breakingChanges = None), + ).lazily + val underlyingSchema: Schema[String] = string.withId(id).addHints(hints) + implicit val schema: Schema[MyPackageStringTrait] = recursive(bijection(underlyingSchema, asBijection)) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/_package/package.scala b/modules/bootstrapped/src/generated/smithy4s/example/_package/package.scala index 28a781875..fffb0cbb9 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/_package/package.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/_package/package.scala @@ -3,5 +3,6 @@ package smithy4s.example package object _package { type MyPackageString = smithy4s.example._package.MyPackageString.Type + type MyPackageStringTrait = smithy4s.example._package.MyPackageStringTrait.Type } \ No newline at end of file diff --git a/modules/bootstrapped/src/generated/smithy4s/example/collision/Class.scala b/modules/bootstrapped/src/generated/smithy4s/example/collision/Class.scala new file mode 100644 index 000000000..8064d82cb --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/collision/Class.scala @@ -0,0 +1,65 @@ +package smithy4s.example.collision + +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.constant +import smithy4s.schema.Schema.recursive +import smithy4s.schema.Schema.union + +sealed trait Class extends scala.Product with scala.Serializable { self => + @inline final def widen: Class = this + def $ordinal: Int + + object project { + def _package: Option[Class.AdtStruct] = Class.AdtStruct.alt.project.lift(self) + } + + def accept[A](visitor: Class.Visitor[A]): A = this match { + case value: Class.AdtStruct => visitor._package(value) + } +} +object Class extends ShapeTag.Companion[Class] { + + def adtStruct():AdtStruct = AdtStruct() + + val id: ShapeId = ShapeId("smithy4s.example.collision", "class") + + val hints: Hints = Hints( + smithy.api.Trait(selector = None, structurallyExclusive = None, conflicts = None, breakingChanges = None), + ).lazily + + final case class AdtStruct() extends Class { + def $ordinal: Int = 0 + } + + object AdtStruct { + val id: ShapeId = ShapeId("smithy4s.example.collision", "AdtStruct") + + val hints: Hints = Hints.empty + + + val schema: Schema[AdtStruct] = constant(AdtStruct()).withId(id).addHints(hints) + + val alt = schema.oneOf[Class]("package") + } + + + trait Visitor[A] { + def _package(value: Class.AdtStruct): A + } + + object Visitor { + trait Default[A] extends Visitor[A] { + def default: A + def _package(value: Class.AdtStruct): A = default + } + } + + implicit val schema: Schema[Class] = recursive(union( + Class.AdtStruct.alt, + ){ + _.$ordinal + }.withId(id).addHints(hints)) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/collision/ReservedNameUnionTrait.scala b/modules/bootstrapped/src/generated/smithy4s/example/collision/ReservedNameUnionTrait.scala new file mode 100644 index 000000000..d1d388b4d --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/collision/ReservedNameUnionTrait.scala @@ -0,0 +1,17 @@ +package smithy4s.example.collision + +import smithy4s.Hints +import smithy4s.Newtype +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.schema.Schema.bijection +import smithy4s.schema.Schema.string + +object ReservedNameUnionTrait extends Newtype[java.lang.String] { + val id: ShapeId = ShapeId("smithy4s.example.collision", "ReservedNameUnionTrait") + val hints: Hints = Hints( + smithy4s.example.collision.Class.AdtStruct().widen, + ).lazily + val underlyingSchema: Schema[java.lang.String] = string.withId(id).addHints(hints) + implicit val schema: Schema[ReservedNameUnionTrait] = bijection(underlyingSchema, asBijection) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/collision/TestReservedNamespaceTrait.scala b/modules/bootstrapped/src/generated/smithy4s/example/collision/TestReservedNamespaceTrait.scala new file mode 100644 index 000000000..58cbc0b3f --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/collision/TestReservedNamespaceTrait.scala @@ -0,0 +1,17 @@ +package smithy4s.example.collision + +import smithy4s.Hints +import smithy4s.Newtype +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.schema.Schema.bijection +import smithy4s.schema.Schema.string + +object TestReservedNamespaceTrait extends Newtype[java.lang.String] { + val id: ShapeId = ShapeId("smithy4s.example.collision", "TestReservedNamespaceTrait") + val hints: Hints = Hints( + smithy4s.example._package.MyPackageStringTrait("test"), + ).lazily + val underlyingSchema: Schema[java.lang.String] = string.withId(id).addHints(hints) + implicit val schema: Schema[TestReservedNamespaceTrait] = bijection(underlyingSchema, asBijection) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/collision/package.scala b/modules/bootstrapped/src/generated/smithy4s/example/collision/package.scala index f7f33c849..bde7a66bd 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/collision/package.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/collision/package.scala @@ -9,6 +9,8 @@ package object collision { type MySet = smithy4s.example.collision.MySet.Type type ReservedKeywordTraitExampleCollection = smithy4s.example.collision.ReservedKeywordTraitExampleCollection.Type type ReservedKeywordTraitExamplePrimitive = smithy4s.example.collision.ReservedKeywordTraitExamplePrimitive.Type + type ReservedNameUnionTrait = smithy4s.example.collision.ReservedNameUnionTrait.Type type String = smithy4s.example.collision.String.Type + type TestReservedNamespaceTrait = smithy4s.example.collision.TestReservedNamespaceTrait.Type } \ No newline at end of file diff --git a/modules/bootstrapped/test/src-js/smithy4s/TimestampSpec.scala b/modules/bootstrapped/test/src-js/smithy4s/TimestampSpec.scala index 0ee6c9cf9..010041363 100644 --- a/modules/bootstrapped/test/src-js/smithy4s/TimestampSpec.scala +++ b/modules/bootstrapped/test/src-js/smithy4s/TimestampSpec.scala @@ -216,4 +216,30 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite { expect.same(tsFromEpochMilli, ts) } } + + property("Truncate to milliseconds precision") { + forAll { (d: Date) => + val epochMilli = d.valueOf().toLong + val ts = Timestamp.fromDate(d).truncateToMillis + + val strippedDate = new Date(0) + strippedDate.setUTCMilliseconds(epochMilli) + val tsFromStrippedDate = Timestamp.fromDate(strippedDate) + + expect.same(ts, tsFromStrippedDate) + } + } + + property("Truncate to seconds precision") { + forAll { (d: Date) => + val epochSecond = (d.valueOf() / 1000).toLong + val ts = Timestamp.fromDate(d).truncateToMillis + + val strippedDate = new Date(0) + strippedDate.setUTCSeconds(epochSecond) + val tsFromStrippedDate = Timestamp.fromDate(strippedDate) + + expect.same(ts, tsFromStrippedDate) + } + } } diff --git a/modules/bootstrapped/test/src-jvm/smithy4s/TimestampSpec.scala b/modules/bootstrapped/test/src-jvm/smithy4s/TimestampSpec.scala index f2b8e40bc..f0b928627 100644 --- a/modules/bootstrapped/test/src-jvm/smithy4s/TimestampSpec.scala +++ b/modules/bootstrapped/test/src-jvm/smithy4s/TimestampSpec.scala @@ -224,4 +224,23 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite { expect.same(tsFromEpochMilli, tsFromStrippedInstant) } } + + property("Truncate to milliseconds precision") { + forAll { (i: Instant) => + val ts = Timestamp.fromInstant(i).truncateToMillis + val strippedInstant = Instant.ofEpochMilli(i.toEpochMilli) + val tsFromStrippedInstant = Timestamp.fromInstant(strippedInstant) + + expect.same(ts, tsFromStrippedInstant) + } + } + + property("Truncate to seconds precision") { + forAll { (i: Instant) => + val ts = Timestamp.fromInstant(i).truncateToSeconds + val tsFromStrippedInstant = Timestamp.fromEpochSecond(i.getEpochSecond) + + expect.same(ts, tsFromStrippedInstant) + } + } } diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/build.sbt b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/build.sbt new file mode 100644 index 000000000..5b4e4ab9d --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/build.sbt @@ -0,0 +1,8 @@ +lazy val root = (project in file(".")) + .enablePlugins(Smithy4sCodegenPlugin) + .settings( + crossScalaVersions := Seq("2.13.15", "3.3.3"), + libraryDependencies ++= Seq( + "com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion.value + ) + ) diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/project/build.properties b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/project/build.properties new file mode 100644 index 000000000..8b9a0b0ab --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.8.0 diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/project/plugins.sbt b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/project/plugins.sbt new file mode 100644 index 000000000..b8589b92c --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/project/plugins.sbt @@ -0,0 +1,9 @@ +sys.props.get("plugin.version") match { + case Some(x) => + addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % x) + case _ => + sys.error( + """|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin + ) +} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/src/main/smithy/simple.smithy b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/src/main/smithy/simple.smithy new file mode 100644 index 000000000..522b18cff --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/src/main/smithy/simple.smithy @@ -0,0 +1,3 @@ +namespace smithy4s.example + +structure Simple {} diff --git a/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/test b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/test new file mode 100644 index 000000000..977249eef --- /dev/null +++ b/modules/codegen-plugin/src/sbt-test/codegen-plugin/cross-builds/test @@ -0,0 +1,19 @@ +# generate for Scala 2.13 +> ++ 2.13 +> compile + +$ exists target/scala-2.13/src_managed/main/smithy/generated-metadata.smithy +$ exists target/scala-2.13/src_managed/main/smithy4s/smithy4s/example/Simple.scala + +> ++ 3 +> compile + +$ exists target/scala-3.3.3/src_managed/main/smithy/generated-metadata.smithy +$ exists target/scala-3.3.3/src_managed/main/smithy4s/smithy4s/example/Simple.scala + +# switch back to Scala 2.13 and compile again; codegen should not happen a second time +> ++ 2.13 +> compile + +$ newer target/scala-3.3.3/src_managed/main/smithy/generated-metadata.smithy target/scala-2.13/src_managed/main/smithy/generated-metadata.smithy +$ newer target/scala-3.3.3/src_managed/main/smithy4s/smithy4s/example/Simple.scala target/scala-2.13/src_managed/main/smithy4s/smithy4s/example/Simple.scala diff --git a/modules/codegen-plugin/src/smithy4s/codegen/Smithy4sCodegenPlugin.scala b/modules/codegen-plugin/src/smithy4s/codegen/Smithy4sCodegenPlugin.scala index d0d15baae..a97ac8a3c 100644 --- a/modules/codegen-plugin/src/smithy4s/codegen/Smithy4sCodegenPlugin.scala +++ b/modules/codegen-plugin/src/smithy4s/codegen/Smithy4sCodegenPlugin.scala @@ -264,7 +264,8 @@ object Smithy4sCodegenPlugin extends AutoPlugin { (config / sourceManaged).value / "smithy" / "generated-metadata.smithy" }, config / smithy4sGeneratedSmithyFiles := { - val cacheFactory = (config / streams).value.cacheStoreFactory + val cacheFactory = + (config / streams).value.cacheStoreFactory.sub(scalaVersion.value) val cached = Tracked.inputChanged[(String, Boolean), Seq[File]]( cacheFactory.make("smithy4sGeneratedSmithyFilesInput") ) { case (changed, (wildcardArg, shouldGenerateOptics)) => @@ -454,14 +455,15 @@ object Smithy4sCodegenPlugin extends AutoPlugin { smithyBuild = smithyBuildValue ) + val cacheStoreFactory = s.cacheStoreFactory.sub(scalaVersion.value) val cached = CachedTask.inputChanged[CodegenArgs, Seq[File]]( - s.cacheStoreFactory.make("input"), + cacheStoreFactory.make("input"), s.log ) { Function.untupled { Tracked.lastOutput[(Boolean, CodegenArgs), Seq[File]]( - s.cacheStoreFactory.make("output") + cacheStoreFactory.make("output") ) { case ((inputChanged, args), outputs) => if (inputChanged || outputs.isEmpty) { s.log.debug(s"[smithy4s] Input changed: $inputChanged") diff --git a/modules/codegen/src/smithy4s/codegen/internals/CollisionAvoidance.scala b/modules/codegen/src/smithy4s/codegen/internals/CollisionAvoidance.scala index f6b6e2f87..b72e2cdce 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/CollisionAvoidance.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/CollisionAvoidance.scala @@ -243,6 +243,11 @@ private[internals] object CollisionAvoidance { case ValidatedNewTypeTN(ref, target) => ValidatedNewTypeTN(modRef(ref), target) case AltTN(ref, altName, alt) => + // note: technically we should probably escape altName here + // but it'd only really break if it matched a capitalized keyword, + // and Scala has none of those, so it's impossible to write a failing test. + // Alt names in this context are always capitalized before being printed + // (Renderer.scala:1614 at the time of writing). AltTN(modRef(ref), altName, alt) case MapTN(values) => MapTN(values) diff --git a/modules/codegen/src/smithy4s/codegen/internals/IR.scala b/modules/codegen/src/smithy4s/codegen/internals/IR.scala index 3519c1d3a..9a74a1db6 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/IR.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/IR.scala @@ -305,7 +305,10 @@ private[internals] object Type { valueHints: List[Hint] ) extends Type case class Ref(namespace: String, name: String) extends Type { - def show = namespace + "." + name + def show: String = NameRef + .splitPath(namespace) + .map(CollisionAvoidance.protectKeyword) + .mkString(".") + "." + name } case class Alias( namespace: String, diff --git a/modules/codegen/src/smithy4s/codegen/internals/LineSegment.scala b/modules/codegen/src/smithy4s/codegen/internals/LineSegment.scala index 1e69019e0..bdcfb1375 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/LineSegment.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/LineSegment.scala @@ -95,13 +95,17 @@ private[codegen] object LineSegment { ) implicit val nameRefShow: Show[NameRef] = Show.show[NameRef](_.asImport) def apply(pkg: String, name: String): NameRef = - NameRef(pkg.split("\\.").toList, name, List.empty) + NameRef(splitPath(pkg), name, List.empty) def apply(fqn: String): NameRef = { - val parts = fqn.split("\\.").toList.toNel.get + val parts = + splitPath(fqn).toNel.getOrElse(sys.error(s"Invalid FQN: $fqn")) NameRef(parts.toList.dropRight(1), parts.last, List.empty) } def apply(fqn: String, typeParams: List[NameRef]): NameRef = apply(fqn).copy(typeParams = typeParams) + + private[internals] def splitPath(pkg: String): List[String] = + pkg.split("\\.").toList } implicit val lineSegmentShow: Show[LineSegment] = Show.show { diff --git a/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala b/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala index ae4de8862..0fd3b734b 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala @@ -711,7 +711,7 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => } }, newline, - obj(product.nameRef, shapeTag(product.nameRef))( + obj(product.nameRef, if (adtParent.isEmpty) shapeTag(product.nameRef) else Line.empty)( renderId(shapeId), newline, renderHintsVal(hints), @@ -776,7 +776,7 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => .appendToLast(if (recursive) ")" else "") } } else { - line"implicit val schema: $Schema_[${product.nameRef}] = $constant_(${product.nameRef}()).withId(id).addHints(hints)" + line"${schemaImplicit}val schema: $Schema_[${product.nameRef}] = $constant_(${product.nameRef}()).withId(id).addHints(hints)" }, renderTypeclasses(product.hints, product.nameRef), additionalLines @@ -1583,7 +1583,7 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => private def renderTypedNode(tn: TypedNode[CString]): CString = tn match { case EnumerationTN(ref, _, _, name) => - line"${ref.show + "." + name + ".widen"}".write + line"${ref.show}.$name.widen".write case StructureTN(ref, fields) => val fieldStrings = fields.map { case (name, FieldTN.RequiredTN(value)) => @@ -1617,7 +1617,9 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => line"${ref.show}.${altName.capitalize}Case.widen".write case AltTN(_, _, AltValueTN.ProductAltTN(alt)) => - alt.runDefault.write + // The `widen` is necessary in Scala 2. + // Without it, there is no ShapeTag to use for the conversion to Hints.Binding. + line"${alt.runDefault}.widen".write case CollectionTN(collectionType, values) => val col = collectionType.tpe diff --git a/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala b/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala index 0ccc14bd2..bcdd5c27d 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala @@ -1237,7 +1237,7 @@ private[codegen] class SmithyToIR( case Some(parent) => val cId = shape.getId val newNs = - cId.getNamespace + "." + parent.getName + cId.getNamespace + "." + parent.getName.capitalize val error = new Exception( s"Shapes annotated with the adtMemberTrait must be structures. $cId is not a structure." ) diff --git a/modules/core/src-js/smithy4s/Timestamp.scala b/modules/core/src-js/smithy4s/Timestamp.scala index e8e695c02..d20e83d36 100644 --- a/modules/core/src-js/smithy4s/Timestamp.scala +++ b/modules/core/src-js/smithy4s/Timestamp.scala @@ -47,6 +47,16 @@ case class Timestamp private (epochSecond: Long, nano: Int) { date } + /** + * @return a copy of this timestamp truncated to a miliseconds precision + */ + def truncateToMillis: Timestamp = copy(nano = (nano / 1000000) * 1000000) + + /** + * @return a copy of this timestamp truncated to a seconds resolution + */ + def truncateToSeconds: Timestamp = copy(nano = 0) + override def toString: String = format(TimestampFormat.DATE_TIME) private[this] def formatToString(internalFormat: Int): String = { diff --git a/modules/core/src-jvm-native/smithy4s/Timestamp.scala b/modules/core/src-jvm-native/smithy4s/Timestamp.scala index ce3664e5c..0e71dac44 100644 --- a/modules/core/src-jvm-native/smithy4s/Timestamp.scala +++ b/modules/core/src-jvm-native/smithy4s/Timestamp.scala @@ -39,6 +39,16 @@ case class Timestamp private (epochSecond: Long, nano: Int) def conciseDate: String = formatToString(2) + /** + * @return a copy of this timestamp truncated to a miliseconds precision + */ + def truncateToMillis: Timestamp = copy(nano = (nano / 1000000) * 1000000) + + /** + * @return a copy of this timestamp truncated to a seconds resolution + */ + def truncateToSeconds: Timestamp = copy(nano = 0) + override def toString: String = format(TimestampFormat.DATE_TIME) private[this] def formatToString(internalFormat: Int): String = { diff --git a/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md b/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md index 1726a52f4..f57de12c0 100644 --- a/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md +++ b/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md @@ -90,6 +90,14 @@ See the section about [unions](../../04-codegen/02-unions.md) for a detailed des By default, optional properties (headers, query parameters, structure fields) that are set to `None` and optional properties that are set to default value will be excluded during encoding process. If you wish to change this so that instead they are included and set to `null` explicitly, you can do so by calling `.withExplicitDefaultsEncoding(true)`. +## Other customisations of JSON codec behaviour + +The underlying JSON codecs can be configured with a number of options to cater to niche usecases, via the `.transformJsonCodecs` method, which takes a function that takes in and returns a +`JsonPayloadCodecCompiler`. For instance, by default, the `NaN` and `Infinity` values are not considered valid during parsing `Float` or `Double` values. This can be amended via +`.transformJsonCodecs(_.configureJsoniterCodecCompiler(_.withInfinitySupport(true)))`. + +The customisations are bound to evolve as we uncover new niche cases that warrant adding new pieces of opt-in behaviour. The default behaviour is kept rather strict as it helps keep competitive performance and safety. + ## Supported traits Here is the list of traits supported by `SimpleRestJson` diff --git a/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala index f4c76f516..6d3e96226 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala @@ -17,7 +17,17 @@ package smithy4s package http4s -object SimpleRestJsonBuilder extends SimpleRestJsonBuilder(1024, false, true) +import smithy4s.json.Json +import smithy4s.json.JsonPayloadCodecCompiler + +object SimpleRestJsonBuilder + extends SimpleRestJsonBuilder( + new internals.SimpleRestJsonCodecs( + jsonCodecs = Json.payloadCodecs, + explicitDefaultsEncoding = false, + hostPrefixInjection = true + ) + ) class SimpleRestJsonBuilder private ( simpleRestJsonCodecs: internals.SimpleRestJsonCodecs @@ -25,6 +35,7 @@ class SimpleRestJsonBuilder private ( simpleRestJsonCodecs ) { + @deprecated(message = "Use .withXXX methods instead", since = "0.18.25") def this( maxArity: Int, explicitDefaultsEncoding: Boolean, @@ -32,7 +43,12 @@ class SimpleRestJsonBuilder private ( ) = this( new internals.SimpleRestJsonCodecs( - maxArity, + Json.payloadCodecs + .withJsoniterCodecCompiler( + Json.jsoniter + .withMaxArity(maxArity) + .withExplicitDefaultsEncoding(explicitDefaultsEncoding) + ), explicitDefaultsEncoding, hostPrefixInjection ) @@ -40,24 +56,28 @@ class SimpleRestJsonBuilder private ( def withMaxArity(maxArity: Int): SimpleRestJsonBuilder = new SimpleRestJsonBuilder( - maxArity, - simpleRestJsonCodecs.explicitDefaultsEncoding, - simpleRestJsonCodecs.hostPrefixInjection + simpleRestJsonCodecs.transformJsonCodecs( + _.configureJsoniterCodecCompiler(_.withMaxArity(maxArity)) + ) ) def withExplicitDefaultsEncoding( explicitDefaultsEncoding: Boolean ): SimpleRestJsonBuilder = new SimpleRestJsonBuilder( - simpleRestJsonCodecs.maxArity, - explicitDefaultsEncoding, - simpleRestJsonCodecs.hostPrefixInjection + simpleRestJsonCodecs.withExplicitDefaultEncoding(explicitDefaultsEncoding) ) def disableHostPrefixInjection(): SimpleRestJsonBuilder = new SimpleRestJsonBuilder( - simpleRestJsonCodecs.maxArity, - simpleRestJsonCodecs.explicitDefaultsEncoding, - false + simpleRestJsonCodecs.withHostPrefixInjection(false) ) + + /** + * Transforms the underlying JSON codec compiler to change its behaviour. + */ + def transformJsonCodecs( + f: JsonPayloadCodecCompiler => JsonPayloadCodecCompiler + ): SimpleRestJsonBuilder = + new SimpleRestJsonBuilder(simpleRestJsonCodecs.transformJsonCodecs(f)) } diff --git a/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala b/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala index b7f7648fa..da153c842 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala @@ -24,7 +24,6 @@ import smithy4s.http.HttpDiscriminator import smithy4s.http.Metadata import smithy4s.http._ import smithy4s.http4s.kernel._ -import smithy4s.json.Json import smithy4s.client._ import smithy4s.codecs.BlobEncoder import cats.syntax.all._ @@ -32,30 +31,36 @@ import org.http4s.Response import org.http4s.Request import org.http4s.Uri import smithy4s.http.HttpMethod +import smithy4s.json.JsonPayloadCodecCompiler // scalafmt: {maxColumn = 120} private[http4s] class SimpleRestJsonCodecs( - val maxArity: Int, + val jsonCodecs: JsonPayloadCodecCompiler, val explicitDefaultsEncoding: Boolean, val hostPrefixInjection: Boolean ) extends SimpleProtocolCodecs { private val hintMask = alloy.SimpleRestJson.protocol.hintMask - private val jsonCodecs = Json.payloadCodecs - .withJsoniterCodecCompiler( - Json.jsoniter - .withHintMask(hintMask) - .withMaxArity(maxArity) - .withExplicitDefaultsEncoding(explicitDefaultsEncoding) + def transformJsonCodecs(f: JsonPayloadCodecCompiler => JsonPayloadCodecCompiler): SimpleRestJsonCodecs = + new SimpleRestJsonCodecs(f(jsonCodecs), explicitDefaultsEncoding, hostPrefixInjection) + + def withExplicitDefaultEncoding(newExplicitDefaultsEncoding: Boolean): SimpleRestJsonCodecs = + new SimpleRestJsonCodecs( + jsonCodecs.configureJsoniterCodecCompiler(_.withExplicitDefaultsEncoding(newExplicitDefaultsEncoding)), + newExplicitDefaultsEncoding, + hostPrefixInjection ) + def withHostPrefixInjection(newHostPrefixInjection: Boolean): SimpleRestJsonCodecs = + new SimpleRestJsonCodecs(jsonCodecs, explicitDefaultsEncoding, newHostPrefixInjection) + // val mediaType = HttpMediaType("application/json") private val payloadEncoders: BlobEncoder.Compiler = - jsonCodecs.encoders + jsonCodecs.configureJsoniterCodecCompiler(_.withHintMask(hintMask)).encoders private val payloadDecoders = - jsonCodecs.decoders + jsonCodecs.configureJsoniterCodecCompiler(_.withHintMask(hintMask)).decoders // Adding X-Amzn-Errortype as well to facilitate interop with Amazon-issued code-generators. private val errorHeaders = List( diff --git a/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala b/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala index a8a8ee8a7..fd032f266 100644 --- a/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala +++ b/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala @@ -78,6 +78,12 @@ trait JsoniterCodecCompiler extends CachedSchemaCompiler[JsonCodec] { */ def withLenientTaggedUnionDecoding: JsoniterCodecCompiler + /** + * Enables lenient decoding of numeric values, where numbers may be carried by JSON strings + * as well as JSON numbers. + */ + def withLenientNumericDecoding: JsoniterCodecCompiler + } object JsoniterCodecCompiler { diff --git a/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala b/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala index 09fbc9bc3..92d3afd50 100644 --- a/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala +++ b/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala @@ -27,7 +27,8 @@ private[smithy4s] case class JsoniterCodecCompilerImpl( infinitySupport: Boolean, preserveMapOrder: Boolean, hintMask: Option[HintMask], - lenientTaggedUnionDecoding: Boolean + lenientTaggedUnionDecoding: Boolean, + lenientNumericDecoding: Boolean ) extends CachedSchemaCompiler.Impl[JCodec] with JsoniterCodecCompiler { @@ -59,6 +60,9 @@ private[smithy4s] case class JsoniterCodecCompilerImpl( def withLenientTaggedUnionDecoding: JsoniterCodecCompiler = copy(lenientTaggedUnionDecoding = true) + def withLenientNumericDecoding: JsoniterCodecCompiler = + copy(lenientNumericDecoding = true) + def fromSchema[A](schema: Schema[A], cache: Cache): JCodec[A] = { val visitor = new SchemaVisitorJCodec( maxArity, @@ -67,6 +71,7 @@ private[smithy4s] case class JsoniterCodecCompilerImpl( flexibleCollectionsSupport, preserveMapOrder, lenientTaggedUnionDecoding, + lenientNumericDecoding, cache ) val amendedSchema = @@ -88,6 +93,7 @@ private[smithy4s] object JsoniterCodecCompilerImpl { flexibleCollectionsSupport = false, preserveMapOrder = false, lenientTaggedUnionDecoding = false, + lenientNumericDecoding = false, hintMask = Some(JsoniterCodecCompiler.defaultHintMask) ) diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index f4df6330c..354b72e81 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -47,10 +47,14 @@ private[smithy4s] class SchemaVisitorJCodec( flexibleCollectionsSupport: Boolean, preserveMapOrder: Boolean, lenientTaggedUnionDecoding: Boolean, + lenientNumericDecoding: Boolean, val cache: CompilationCache[JCodec] ) extends SchemaVisitor.Cached[JCodec] { self => private val emptyMetadata: MMap[String, Any] = MMap.empty + private val allowJsonStringNumerics = + lenientNumericDecoding || infinitySupport + object PrimitiveJCodecs { val boolean: JCodec[Boolean] = new JCodec[Boolean] { @@ -80,153 +84,194 @@ private[smithy4s] class SchemaVisitorJCodec( def encodeKey(x: String, out: JsonWriter): Unit = out.writeKey(x) } - val int: JCodec[Int] = - new JCodec[Int] { - def expecting: String = "int" + private abstract class NumericJCodec[A] extends JCodec[A] { + def decodeJsonNumber(cursor: Cursor, in: JsonReader): A + // Allows numerics to be received as JSON Strings + def decodeJsonString(cursor: Cursor, in: JsonReader): A - def decodeValue(cursor: Cursor, in: JsonReader): Int = in.readInt() + final def decodeValue(cursor: Cursor, in: JsonReader): A = + if (allowJsonStringNumerics) { + if (in.isNextToken('"')) { + in.rollbackToken() + decodeJsonString(cursor, in) + } else { + in.rollbackToken() + decodeJsonNumber(cursor, in) + } + } else { + decodeJsonNumber(cursor, in) + } + } - def encodeValue(x: Int, out: JsonWriter): Unit = out.writeVal(x) + val int: JCodec[Int] = new NumericJCodec[Int] { + def expecting: String = "int" - def decodeKey(in: JsonReader): Int = in.readKeyAsInt() + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Int = + in.readInt() - def encodeKey(x: Int, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeJsonString(cursor: Cursor, in: JsonReader): Int = + in.readStringAsInt() - val long: JCodec[Long] = - new JCodec[Long] { - def expecting: String = "long" + def encodeValue(x: Int, out: JsonWriter): Unit = out.writeVal(x) - def decodeValue(cursor: Cursor, in: JsonReader): Long = in.readLong() + def decodeKey(in: JsonReader): Int = in.readKeyAsInt() - def encodeValue(x: Long, out: JsonWriter): Unit = out.writeVal(x) + def encodeKey(x: Int, out: JsonWriter): Unit = out.writeKey(x) + } - def decodeKey(in: JsonReader): Long = in.readKeyAsLong() + val long: JCodec[Long] = new NumericJCodec[Long] { + def expecting: String = "long" - def encodeKey(x: Long, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Long = + in.readLong() - private val efficientFloat: JCodec[Float] = - new JCodec[Float] { - def expecting: String = "float" + def decodeJsonString(cursor: Cursor, in: JsonReader): Long = + in.readStringAsLong() - def decodeValue(cursor: Cursor, in: JsonReader): Float = in.readFloat() + def encodeValue(x: Long, out: JsonWriter): Unit = out.writeVal(x) - def encodeValue(x: Float, out: JsonWriter): Unit = out.writeVal(x) + def decodeKey(in: JsonReader): Long = in.readKeyAsLong() - def decodeKey(in: JsonReader): Float = in.readKeyAsFloat() + def encodeKey(x: Long, out: JsonWriter): Unit = out.writeKey(x) + } - def encodeKey(x: Float, out: JsonWriter): Unit = out.writeKey(x) - } + private val efficientFloat: JCodec[Float] = new NumericJCodec[Float] { + def expecting: String = "float" - private val infinityAllowingFloat: JCodec[Float] = new JCodec[Float] { - val expecting: String = "JSON number for numeric values" + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Float = + in.readFloat() - def decodeValue(cursor: Cursor, in: JsonReader): Float = - if (in.isNextToken('"')) { - in.rollbackToken() + def decodeJsonString(cursor: Cursor, in: JsonReader): Float = + in.readStringAsFloat() + + def encodeValue(x: Float, out: JsonWriter): Unit = out.writeVal(x) + + def decodeKey(in: JsonReader): Float = in.readKeyAsFloat() + + def encodeKey(x: Float, out: JsonWriter): Unit = out.writeKey(x) + } + + private val infinityAllowingFloat: JCodec[Float] = + new NumericJCodec[Float] { + val expecting: String = "JSON number for numeric values" + + def decodeJsonString(cursor: Cursor, in: JsonReader): Float = { + in.setMark() val len = in.readStringAsCharBuf() if (in.isCharBufEqualsTo(len, "NaN")) Float.NaN else if (in.isCharBufEqualsTo(len, "Infinity")) Float.PositiveInfinity else if (in.isCharBufEqualsTo(len, "-Infinity")) Float.NegativeInfinity - else in.decodeError("illegal float") - } else { - in.rollbackToken() + else { + in.rollbackToMark() + in.readStringAsFloat() + } + } + + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Float = { in.readFloat() } - def encodeValue(f: Float, out: JsonWriter): Unit = - if (java.lang.Float.isFinite(f)) out.writeVal(f) - else - out.writeNonEscapedAsciiVal { - if (f != f) "NaN" - else if (f >= 0) "Infinity" - else "-Infinity" - } + def encodeValue(f: Float, out: JsonWriter): Unit = + if (java.lang.Float.isFinite(f)) out.writeVal(f) + else + out.writeNonEscapedAsciiVal { + if (f != f) "NaN" + else if (f >= 0) "Infinity" + else "-Infinity" + } - def decodeKey(in: JsonReader): Float = ??? + def decodeKey(in: JsonReader): Float = ??? - def encodeKey(x: Float, out: JsonWriter): Unit = ??? - } + def encodeKey(x: Float, out: JsonWriter): Unit = ??? + } val float: JCodec[Float] = if (infinitySupport) infinityAllowingFloat else efficientFloat - private val efficientDouble: JCodec[Double] = - new JCodec[Double] { - def expecting: String = "double" + private val efficientDouble: JCodec[Double] = new NumericJCodec[Double] { + def expecting: String = "double" - def decodeValue(cursor: Cursor, in: JsonReader): Double = - in.readDouble() + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Double = + in.readDouble() - def encodeValue(x: Double, out: JsonWriter): Unit = out.writeVal(x) + def decodeJsonString(cursor: Cursor, in: JsonReader): Double = + in.readStringAsDouble() - def decodeKey(in: JsonReader): Double = in.readKeyAsDouble() + def encodeValue(x: Double, out: JsonWriter): Unit = out.writeVal(x) - def encodeKey(x: Double, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeKey(in: JsonReader): Double = in.readKeyAsDouble() + + def encodeKey(x: Double, out: JsonWriter): Unit = out.writeKey(x) + } - private val infinityAllowingDouble: JCodec[Double] = new JCodec[Double] { - val expecting: String = "JSON number for numeric values" + private val infinityAllowingDouble: JCodec[Double] = + new NumericJCodec[Double] { + val expecting: String = "JSON number for numeric values" - def decodeValue(cursor: Cursor, in: JsonReader): Double = - if (in.isNextToken('"')) { - in.rollbackToken() + def decodeJsonString(cursor: Cursor, in: JsonReader): Double = { + in.setMark() val len = in.readStringAsCharBuf() if (in.isCharBufEqualsTo(len, "NaN")) Double.NaN else if (in.isCharBufEqualsTo(len, "Infinity")) Double.PositiveInfinity else if (in.isCharBufEqualsTo(len, "-Infinity")) Double.NegativeInfinity - else in.decodeError("illegal double") - } else { - in.rollbackToken() - in.readDouble() + else { + in.rollbackToMark() + in.readStringAsDouble() + } } - def encodeValue(d: Double, out: JsonWriter): Unit = - if (java.lang.Double.isFinite(d)) out.writeVal(d) - else - out.writeNonEscapedAsciiVal { - if (d != d) "NaN" - else if (d >= 0) "Infinity" - else "-Infinity" - } + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Double = + in.readDouble() - def decodeKey(in: JsonReader): Double = ??? + def encodeValue(d: Double, out: JsonWriter): Unit = + if (java.lang.Double.isFinite(d)) out.writeVal(d) + else + out.writeNonEscapedAsciiVal { + if (d != d) "NaN" + else if (d >= 0) "Infinity" + else "-Infinity" + } - def encodeKey(x: Double, out: JsonWriter): Unit = ??? - } + def decodeKey(in: JsonReader): Double = ??? + + def encodeKey(x: Double, out: JsonWriter): Unit = ??? + } val double: JCodec[Double] = if (infinitySupport) infinityAllowingDouble else efficientDouble - val short: JCodec[Short] = - new JCodec[Short] { - def expecting: String = "short" + val short: JCodec[Short] = new NumericJCodec[Short] { + def expecting: String = "short" - def decodeValue(cursor: Cursor, in: JsonReader): Short = in.readShort() + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Short = + in.readShort() - def encodeValue(x: Short, out: JsonWriter): Unit = out.writeVal(x) + def decodeJsonString(cursor: Cursor, in: JsonReader): Short = + in.readStringAsShort() - def decodeKey(in: JsonReader): Short = in.readKeyAsShort() + def encodeValue(x: Short, out: JsonWriter): Unit = out.writeVal(x) - def encodeKey(x: Short, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeKey(in: JsonReader): Short = in.readKeyAsShort() - val byte: JCodec[Byte] = - new JCodec[Byte] { - def expecting: String = "byte" + def encodeKey(x: Short, out: JsonWriter): Unit = out.writeKey(x) + } - def decodeValue(cursor: Cursor, in: JsonReader): Byte = in.readByte() + val byte: JCodec[Byte] = new NumericJCodec[Byte] { + def expecting: String = "byte" - def encodeValue(x: Byte, out: JsonWriter): Unit = out.writeVal(x) + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Byte = in.readByte() - def decodeKey(in: JsonReader): Byte = in.readKeyAsByte() + def decodeJsonString(cursor: Cursor, in: JsonReader): Byte = in.readByte() - def encodeKey(x: Byte, out: JsonWriter): Unit = out.writeKey(x) - } + def encodeValue(x: Byte, out: JsonWriter): Unit = out.writeVal(x) + + def decodeKey(in: JsonReader): Byte = in.readKeyAsByte() + + def encodeKey(x: Byte, out: JsonWriter): Unit = out.writeKey(x) + } val bytes: JCodec[Blob] = new JCodec[Blob] { diff --git a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala index 398615baf..c0d9ac9f0 100644 --- a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala +++ b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala @@ -24,6 +24,30 @@ import munit._ class SchemaVisitorJCodecCustomisationTests extends FunSuite { + case class FooInt(i: Int) + object FooInt { + implicit val schema: Schema[FooInt] = { + val i = int.required[FooInt]("i", _.i) + struct(i)(FooInt.apply) + } + } + + case class FooShort(s: Short) + object FooShort { + implicit val schema: Schema[FooShort] = { + val s = short.required[FooShort]("s", _.s) + struct(s)(FooShort.apply) + } + } + + case class FooLong(l: Long) + object FooLong { + implicit val schema: Schema[FooLong] = { + val s = long.required[FooLong]("l", _.l) + struct(s)(FooLong.apply) + } + } + case class FooDouble(d: Double) object FooDouble { implicit val schema: Schema[FooDouble] = { @@ -178,4 +202,39 @@ class SchemaVisitorJCodecCustomisationTests extends FunSuite { expect(result.f.isNegInfinity) } + + test("decoding JSON string as a Float") { + val input = """{"f" : "1.1" }""" + val result = readFromString[FooFloat](input) + + expect.eql(result.f, 1.1f) + } + + test("decoding JSON string as a Double") { + val input = """{"d" : "1.1" }""" + val result = readFromString[FooDouble](input) + + expect.eql(result.d, 1.1d) + } + + test("decoding JSON string as an Int") { + val input = """{"i" : "1" }""" + val result = readFromString[FooInt](input) + + expect.eql(result.i, 1) + } + + test("decoding JSON string as a Long") { + val input = """{"l" : "1" }""" + val result = readFromString[FooLong](input) + + expect.eql(result.l, 1L) + } + + test("decoding JSON string as a Short") { + val input = """{"s" : "1" }""" + val result = readFromString[FooShort](input) + + expect.eql(result.s, 1.toShort) + } } diff --git a/sampleSpecs/reservedNamespace.smithy b/sampleSpecs/reservedNamespace.smithy index af20e2088..d263b1488 100644 --- a/sampleSpecs/reservedNamespace.smithy +++ b/sampleSpecs/reservedNamespace.smithy @@ -3,3 +3,6 @@ $version: "2.0" namespace smithy4s.example.package string MyPackageString + +@trait +string MyPackageStringTrait diff --git a/sampleSpecs/reservednames.smithy b/sampleSpecs/reservednames.smithy index 9991f479d..ec39da06c 100644 --- a/sampleSpecs/reservednames.smithy +++ b/sampleSpecs/reservednames.smithy @@ -4,6 +4,7 @@ namespace smithy4s.example.collision use alloy#simpleRestJson use smithy4s.example.package#MyPackageString +use smithy4s.example.package#MyPackageStringTrait @simpleRestJson service ReservedNameService { @@ -67,6 +68,10 @@ structure TestReservedNamespaceImport { package: MyPackageString } +// Regression test for https://github.com/disneystreaming/smithy4s/issues/1601 +@MyPackageStringTrait("test") +string TestReservedNamespaceTrait + // trait def @trait structure reservedKeywordStructTrait { @@ -146,3 +151,17 @@ structure Scala3ReservedKeywords { export: String enum: String } + +@smithy4s.meta#adt +@trait +union class { + package: AdtStruct +} + +structure AdtStruct {} + +// Regression test for https://github.com/disneystreaming/smithy4s/issues/1601 +@class(package: { + +}) +string ReservedNameUnionTrait