diff --git a/CHANGELOG.md b/CHANGELOG.md index d062e7ebe..47dc4d3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.18.5 + +* When encoding to `application/x-www-form-urlencoded`, omit optional fields set to the field's default value. + # 0.18.4 * Changes the behaviour of `Field#getUnlessDefault` and `Field#foreachUnlessDefault` to always take the value into consideration when the `smithy.api#required` trait diff --git a/modules/bootstrapped/test/src/smithy4s/http/internals/UrlFormDataEncoderDecoderSchemaVisitorSpec.scala b/modules/bootstrapped/test/src/smithy4s/http/internals/UrlFormDataEncoderDecoderSchemaVisitorSpec.scala index e9720408e..e10734756 100644 --- a/modules/bootstrapped/test/src/smithy4s/http/internals/UrlFormDataEncoderDecoderSchemaVisitorSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/http/internals/UrlFormDataEncoderDecoderSchemaVisitorSpec.scala @@ -175,6 +175,89 @@ object UrlFormDataEncoderDecoderSchemaVisitorSpec extends SimpleIOSuite { ) } + pureTest( + "struct: default (but not required) values are omitted from encoded output" + ) { + val defaultValue = "default" + case class Foo(x: Int, y: String) + object Foo { + implicit val schema: Schema[Foo] = { + val x = int.required[Foo]("x", _.x) + val y = smithy4s.schema + .Field[Foo, String]("x", string, _.y) + .addHints( + smithy.api.Default(smithy4s.Document.fromString(defaultValue)) + ) + struct(x, y)(Foo.apply) + } + } + + expect.same( + UrlForm + .Encoder( + capitalizeStructAndUnionMemberNames = false + ) + .fromSchema(Foo.schema) + .encode(Foo(42, defaultValue)) + .render, + "x=42" + ) + } + + pureTest("struct: required default values are included in encoded output") { + val defaultValue = "default" + case class Foo(x: Int, y: String) + object Foo { + implicit val schema: Schema[Foo] = { + val x = int.required[Foo]("x", _.x) + val y = string + .required[Foo]("y", _.y) + .addHints( + smithy.api.Default(smithy4s.Document.fromString(defaultValue)) + ) + struct(x, y)(Foo.apply) + } + } + + expect.same( + UrlForm + .Encoder( + capitalizeStructAndUnionMemberNames = false + ) + .fromSchema(Foo.schema) + .encode(Foo(42, defaultValue)) + .render, + "x=42&y=default" + ) + } + + pureTest("struct: optional default values are omitted from encoded output") { + val defaultValue = "default" + case class Foo(x: Int, y: Option[String]) + object Foo { + implicit val schema: Schema[Foo] = { + val x = int.required[Foo]("x", _.x) + val y = string + .optional[Foo]("y", _.y) + .addHints( + smithy.api.Default(smithy4s.Document.fromString(defaultValue)) + ) + struct(x, y)(Foo.apply) + } + } + + expect.same( + UrlForm + .Encoder( + capitalizeStructAndUnionMemberNames = false + ) + .fromSchema(Foo.schema) + .encode(Foo(42, Some(defaultValue))) + .render, + "x=42" + ) + } + pureTest("list") { case class Foo(foos: List[Int]) object Foo { diff --git a/modules/core/src/smithy4s/http/internals/UrlFormDataEncoderSchemaVisitor.scala b/modules/core/src/smithy4s/http/internals/UrlFormDataEncoderSchemaVisitor.scala index 30ea86c76..5d790ea96 100644 --- a/modules/core/src/smithy4s/http/internals/UrlFormDataEncoderSchemaVisitor.scala +++ b/modules/core/src/smithy4s/http/internals/UrlFormDataEncoderSchemaVisitor.scala @@ -136,8 +136,14 @@ private[http] class UrlFormDataEncoderSchemaVisitor( make: IndexedSeq[Any] => S ): UrlFormDataEncoder[S] = { def fieldEncoder[A](field: Field[S, A]): UrlFormDataEncoder[S] = - compile(field.schema) - .contramap(field.get) + new UrlFormDataEncoder[S] { + private val cachedEncoder = compile(field.schema) + override def encode(value: S): List[UrlForm.FormData] = + field + .getUnlessDefault(value) + .toList + .flatMap(cachedEncoder.encode) + } .prepend(getKey(field.hints, field.label)) val encoders = fields.map(fieldEncoder(_)) struct => encoders.toList.flatMap(_.encode(struct))