Skip to content

Commit 1c1e16d

Browse files
feat: Add support for flexible defaults encoding (#1652)
* feat: Add support for more flexible defaults encoding * Flip the condition * Add skipIfEmptyCollection variants * tweaks for @deprecated annotation * Add empty line after header * Apply review comments * Rename to FieldSkipCompiler * Mark explicitDefaultsEncoding as deprecated * Create monoid instance for FieldSkipCompiler * Fix compilation for scala3 * Fix bijection and refinement * Rewrite tests and convert FieldSkipCompiler instances to case objects * Add lazy implementation * Add test for recursive list * Remove asInstanceOf * cleanup * Revert accidental rename * Change shouldSkip to shouldRender * Rewrite other occurences of explicitDefaults to new encoding * Drop monoid instance for FieldSkipCompiler * Rename FieldSkipCompiler to FieldFilter * Replace combine method with regular boolean operations * Rename rest of the methods to use new name * More renames * More concise formatting * Remove existential type from the interface * Add comment about calling .value in Lazy case * Update changelog * Update documentation * Fix SchemaVisitorMetadataWriter * Rename FieldFilter instances * Rename compileOptional to compileNonRequired * Rename SkipUnsetAndDefaultOptionValues to Default * Fix compilation and add test case for bijection schema * Add test for refined schema * Update docs * Move FieldFilter to smithy4s.schema * Fix typo * Rename SkipDefaultOptionValues to SkipNonRequiredDefaultValues * Add missing changelog entry * Remove redundant imports after refactor --------- Co-authored-by: ghostbuster91 <[email protected]> Co-authored-by: Jakub Kozłowski <[email protected]>
1 parent 2ebe2e6 commit 1c1e16d

File tree

20 files changed

+775
-122
lines changed

20 files changed

+775
-122
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Thank you!
1111
* Use correct cross path for protobuf-runtime-scala in [#1648](https://github.com/disneystreaming/smithy4s/pull/1648)
1212
* Force rendering package object when a validated newtype is present in [#1656](https://github.com/disneystreaming/smithy4s/pull/1656)
1313
* Improve performance of ADT trait validator on larger Smithy models in [#1573](https://github.com/disneystreaming/smithy4s/pull/1573)
14+
* Move memoization of default values from Field to Schema in [#1651](https://github.com/disneystreaming/smithy4s/pull/1651)
15+
* Add support for more flexible encoding of defaults in [#1652](https://github.com/disneystreaming/smithy4s/pull/1652). This brings `FieldFilter` abstraction that replaces `explicitDefaultsEncoding`.
1416

1517
# 0.18.29
1618

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package smithy4s.example
2+
3+
import smithy4s.Hints
4+
import smithy4s.Newtype
5+
import smithy4s.Schema
6+
import smithy4s.ShapeId
7+
import smithy4s.schema.Schema.bijection
8+
import smithy4s.schema.Schema.list
9+
10+
object RecursiveList extends Newtype[List[RecursiveListWrapper]] {
11+
val id: ShapeId = ShapeId("smithy4s.example", "RecursiveList")
12+
val hints: Hints = Hints.empty
13+
val underlyingSchema: Schema[List[RecursiveListWrapper]] = list(RecursiveListWrapper.schema).withId(id).addHints(hints)
14+
implicit val schema: Schema[RecursiveList] = bijection(underlyingSchema, asBijection)
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package smithy4s.example
2+
3+
import smithy4s.Hints
4+
import smithy4s.Schema
5+
import smithy4s.ShapeId
6+
import smithy4s.ShapeTag
7+
import smithy4s.schema.Schema.recursive
8+
import smithy4s.schema.Schema.struct
9+
10+
final case class RecursiveListWrapper(items: List[smithy4s.example.RecursiveListWrapper])
11+
12+
object RecursiveListWrapper extends ShapeTag.Companion[RecursiveListWrapper] {
13+
val id: ShapeId = ShapeId("smithy4s.example", "RecursiveListWrapper")
14+
15+
val hints: Hints = Hints.empty
16+
17+
// constructor using the original order from the spec
18+
private def make(items: List[smithy4s.example.RecursiveListWrapper]): RecursiveListWrapper = RecursiveListWrapper(items)
19+
20+
implicit val schema: Schema[RecursiveListWrapper] = recursive(struct(
21+
RecursiveList.underlyingSchema.required[RecursiveListWrapper]("items", _.items),
22+
)(make).withId(id).addHints(hints))
23+
}

modules/bootstrapped/src/generated/smithy4s/example/package.scala

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ package object example {
9494
type PersonPhoneNumber = smithy4s.example.PersonPhoneNumber.Type
9595
type PublisherId = smithy4s.example.PublisherId.Type
9696
type PublishersList = smithy4s.example.PublishersList.Type
97+
type RecursiveList = smithy4s.example.RecursiveList.Type
9798
type SomeIndexSeq = smithy4s.example.SomeIndexSeq.Type
9899
type SomeInt = smithy4s.example.SomeInt.Type
99100
type SomeValue = smithy4s.example.SomeValue.Type

modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala

+250-11
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,24 @@ package smithy4s
1919
import smithy.api.JsonName
2020
import smithy.api.Default
2121
import smithy4s.example.IntList
22+
import smithy4s.example.RecursiveListWrapper
2223
import alloy.Discriminated
2324
import alloy.JsonUnknown
2425
import munit._
2526
import smithy4s.example.DefaultNullsOperationOutput
2627
import alloy.Untagged
2728
import smithy4s.example.TimestampOperationInput
2829
import scala.util.Try
30+
import smithy4s.schema.FieldFilter
31+
import smithy4s.refined.NonEmptyList
2932

3033
class DocumentSpec() extends FunSuite {
3134

35+
private case class TestCase(
36+
expectedToSkip: Boolean,
37+
strategy: FieldFilter
38+
)
39+
3240
test("Recursive document codecs should not blow up the stack") {
3341
val recursive: IntList = IntList(1, Some(IntList(2, Some(IntList(3)))))
3442

@@ -466,11 +474,10 @@ class DocumentSpec() extends FunSuite {
466474
"document encoder - all default values + explicit defaults encoding = true"
467475
) {
468476
val result = Document.Encoder
469-
.withExplicitDefaultsEncoding(true)
470-
.fromSchema(
471-
DefaultNullsOperationOutput.schema
472-
)
477+
.withFieldFilter(FieldFilter.EncodeAll)
478+
.fromSchema(DefaultNullsOperationOutput.schema)
473479
.encode(DefaultNullsOperationOutput())
480+
474481
expect.same(
475482
Document.obj(
476483
"optional" -> Document.nullDoc,
@@ -492,10 +499,8 @@ class DocumentSpec() extends FunSuite {
492499
"document encoder - all default values + explicit defaults encoding = false"
493500
) {
494501
val result = Document.Encoder
495-
.withExplicitDefaultsEncoding(false)
496-
.fromSchema(
497-
DefaultNullsOperationOutput.schema
498-
)
502+
.withFieldFilter(FieldFilter.Default)
503+
.fromSchema(DefaultNullsOperationOutput.schema)
499504
.encode(DefaultNullsOperationOutput())
500505
expect.same(
501506
Document.obj(
@@ -508,11 +513,57 @@ class DocumentSpec() extends FunSuite {
508513
)
509514
}
510515

516+
test(
517+
"document encoder - FieldFilter.SkipEmptyOptionals and keep defaults"
518+
) {
519+
val result = Document.Encoder
520+
.withFieldFilter(FieldFilter.SkipUnsetOptions)
521+
.fromSchema(DefaultNullsOperationOutput.schema)
522+
.encode(DefaultNullsOperationOutput())
523+
524+
expect.same(
525+
Document.obj(
526+
"optionalWithDefault" -> Document.fromString("optional-default"),
527+
"requiredWithDefault" -> Document.fromString("required-default"),
528+
"optionalHeaderWithDefault" -> Document.fromString(
529+
"optional-header-with-default"
530+
),
531+
"requiredHeaderWithDefault" -> Document.fromString(
532+
"required-header-with-default"
533+
)
534+
),
535+
result
536+
)
537+
538+
}
539+
540+
test(
541+
"document encoder - FieldFilter.SkipEmptyOptionals and keep defaults"
542+
) {
543+
val result = Document.Encoder
544+
.withFieldFilter(FieldFilter.SkipNonRequiredDefaultValues)
545+
.fromSchema(DefaultNullsOperationOutput.schema)
546+
.encode(DefaultNullsOperationOutput())
547+
548+
expect.same(
549+
Document.obj(
550+
"optional" -> Document.nullDoc,
551+
"optionalHeader" -> Document.nullDoc,
552+
"requiredWithDefault" -> Document.fromString("required-default"),
553+
"requiredHeaderWithDefault" -> Document.fromString(
554+
"required-header-with-default"
555+
)
556+
),
557+
result
558+
)
559+
560+
}
561+
511562
test(
512563
"document encoder - default values overrides + explicit defaults encoding = true"
513564
) {
514565
val result = Document.Encoder
515-
.withExplicitDefaultsEncoding(true)
566+
.withFieldFilter(FieldFilter.EncodeAll)
516567
.fromSchema(DefaultNullsOperationOutput.schema)
517568
.encode(
518569
DefaultNullsOperationOutput(
@@ -552,7 +603,7 @@ class DocumentSpec() extends FunSuite {
552603
"document encoder - default values overrides + explicit defaults encoding = false"
553604
) {
554605
val result = Document.Encoder
555-
.withExplicitDefaultsEncoding(false)
606+
.withFieldFilter(FieldFilter.Default)
556607
.fromSchema(DefaultNullsOperationOutput.schema)
557608
.encode(
558609
DefaultNullsOperationOutput(
@@ -586,7 +637,6 @@ class DocumentSpec() extends FunSuite {
586637
),
587638
result
588639
)
589-
590640
}
591641

592642
test("Document encoder - timestamp defaults") {
@@ -1146,6 +1196,195 @@ class DocumentSpec() extends FunSuite {
11461196
)
11471197
}
11481198

1199+
List(
1200+
TestCase(expectedToSkip = false, FieldFilter.EncodeAll),
1201+
TestCase(expectedToSkip = false, FieldFilter.SkipEmptyOptionalCollection),
1202+
TestCase(expectedToSkip = true, FieldFilter.SkipEmptyCollection)
1203+
).foreach { case TestCase(expectedToSkip, strategy) =>
1204+
val skipNotSkip = if (expectedToSkip) "skip" else "not skip"
1205+
val expected =
1206+
if (expectedToSkip) Document.obj()
1207+
else
1208+
Document.obj(
1209+
"items" -> Document.array()
1210+
)
1211+
1212+
test(s"should ${skipNotSkip} rendering of required lists if it is empty and ${strategy} is used") {
1213+
case class MyStruct(
1214+
items: List[String]
1215+
)
1216+
val arr = list(string).required[MyStruct]("items", _.items)
1217+
val structSchema = struct(arr)(MyStruct.apply)
1218+
1219+
val result = Document.Encoder
1220+
.withFieldFilter(strategy)
1221+
.fromSchema(structSchema)
1222+
.encode(MyStruct(List.empty))
1223+
1224+
expect.same(expected, result)
1225+
}
1226+
1227+
}
1228+
1229+
List(
1230+
TestCase(expectedToSkip = false, FieldFilter.EncodeAll),
1231+
TestCase(expectedToSkip = false, FieldFilter.SkipEmptyOptionalCollection),
1232+
TestCase(expectedToSkip = true, FieldFilter.SkipEmptyCollection)
1233+
).foreach { case TestCase(expectedToSkip, strategy) =>
1234+
val skipNotSkip = if (expectedToSkip) "skip" else "not skip"
1235+
val expected =
1236+
if (expectedToSkip) Document.obj()
1237+
else
1238+
Document.obj(
1239+
"items" -> Document.obj()
1240+
)
1241+
1242+
test(s"should ${skipNotSkip} rendering of required map if it is empty and ${strategy} is used") {
1243+
case class MyStruct(
1244+
items: Map[String, Int]
1245+
)
1246+
val arr = map(string, int).required[MyStruct]("items", _.items)
1247+
val structSchema = struct(arr)(MyStruct.apply)
1248+
1249+
val result = Document.Encoder
1250+
.withFieldFilter(strategy)
1251+
.fromSchema(structSchema)
1252+
.encode(MyStruct(Map.empty))
1253+
1254+
expect.same(
1255+
result,
1256+
expected
1257+
)
1258+
}
1259+
}
1260+
1261+
List(
1262+
TestCase(expectedToSkip = false, FieldFilter.EncodeAll),
1263+
TestCase(expectedToSkip = true, FieldFilter.SkipEmptyCollection),
1264+
TestCase(expectedToSkip = true, FieldFilter.SkipEmptyOptionalCollection)
1265+
).foreach { case TestCase(expectedToSkip, strategy) =>
1266+
val skipNotSkip = if (expectedToSkip) "skip" else "not skip"
1267+
val expected =
1268+
if (expectedToSkip) Document.obj()
1269+
else
1270+
Document.obj(
1271+
"items" -> Document.obj()
1272+
)
1273+
1274+
test(s"should ${skipNotSkip} rendering of optional map if it is empty and ${strategy} is used") {
1275+
case class MyStruct(
1276+
items: Option[Map[String, Int]]
1277+
)
1278+
val arr = map(string, int).optional[MyStruct]("items", _.items)
1279+
val structSchema = struct(arr)(MyStruct.apply)
1280+
1281+
val result = Document.Encoder
1282+
.withFieldFilter(strategy)
1283+
.fromSchema(structSchema)
1284+
.encode(MyStruct(Some(Map.empty)))
1285+
1286+
expect.same(result, expected)
1287+
}
1288+
1289+
}
1290+
1291+
List(
1292+
TestCase(expectedToSkip = false, FieldFilter.EncodeAll),
1293+
TestCase(expectedToSkip = true, FieldFilter.SkipEmptyCollection),
1294+
TestCase(expectedToSkip = true, FieldFilter.SkipEmptyOptionalCollection)
1295+
).foreach { case TestCase(expectedToSkip, strategy) =>
1296+
val skipNotSkip = if (expectedToSkip) "skip" else "not skip"
1297+
val expected =
1298+
if (expectedToSkip) Document.obj()
1299+
else
1300+
Document.obj(
1301+
"items" -> Document.array()
1302+
)
1303+
1304+
test(s"should ${skipNotSkip} rendering of optional lists if it is empty and ${strategy} is used") {
1305+
case class MyStruct(
1306+
items: Option[List[String]]
1307+
)
1308+
val arr = list(string).optional[MyStruct]("items", _.items)
1309+
val structSchema = struct(arr)(MyStruct.apply)
1310+
1311+
val result = Document.Encoder
1312+
.withFieldFilter(strategy)
1313+
.fromSchema(structSchema)
1314+
.encode(MyStruct(Some(List.empty)))
1315+
1316+
expect.same(result, expected)
1317+
}
1318+
1319+
}
1320+
1321+
test("Recursive document structures should not blow up FieldFilter") {
1322+
val recursive: RecursiveListWrapper =
1323+
RecursiveListWrapper(List(RecursiveListWrapper(List(RecursiveListWrapper(List.empty)))))
1324+
1325+
val document = Document.Encoder
1326+
.withFieldFilter(FieldFilter.SkipEmptyCollection)
1327+
.fromSchema(RecursiveListWrapper.schema)
1328+
.encode(recursive)
1329+
import Document._
1330+
val expectedDocument =
1331+
obj("items" -> array(obj("items" -> array(obj()))))
1332+
1333+
expect(document == expectedDocument)
1334+
}
1335+
1336+
test("FieldFilter should work with bijection schema") {
1337+
import Document._
1338+
sealed trait MyEnum[+A] {}
1339+
case object MyEnum {
1340+
case object Empty extends MyEnum[Nothing]
1341+
case class NonEmpty[A](value: A) extends MyEnum[A]
1342+
}
1343+
case class MyStruct(
1344+
items: Option[MyEnum[List[String]]]
1345+
)
1346+
val arr = list(string).option
1347+
.biject(to = _.map(a => MyEnum.NonEmpty(a): MyEnum[List[String]]))(from = _.map {
1348+
case MyEnum.NonEmpty(list) => list
1349+
case MyEnum.Empty => List.empty
1350+
})
1351+
.field[MyStruct]("items", _.items)
1352+
val structSchema = struct(arr)(MyStruct.apply)
1353+
1354+
val result = Document.Encoder
1355+
.withFieldFilter(FieldFilter.SkipEmptyOptionalCollection)
1356+
.fromSchema(structSchema)
1357+
.encode(MyStruct(Some(MyEnum.NonEmpty(List("a", "b")))))
1358+
1359+
val expectedDocument = obj("items" -> array(fromString("a"), fromString("b")))
1360+
expect.same(result, expectedDocument)
1361+
}
1362+
1363+
test("FieldFilter should work with refined schema") {
1364+
import Document._
1365+
case class MyStruct(
1366+
items: Option[NonEmptyList[String]]
1367+
)
1368+
val arr = list(string)
1369+
.refined[NonEmptyList[String]](smithy4s.example.NonEmptyListFormat())
1370+
.option
1371+
.field[MyStruct]("items", _.items)
1372+
val structSchema = struct(arr)(MyStruct.apply)
1373+
1374+
val nonEmptyList = NonEmptyList(List("a", "b")) match {
1375+
case Right(nel) => nel
1376+
case Left(err) => sys.error(err)
1377+
}
1378+
1379+
val result = Document.Encoder
1380+
.withFieldFilter(FieldFilter.SkipEmptyOptionalCollection)
1381+
.fromSchema(structSchema)
1382+
.encode(MyStruct(Some(nonEmptyList)))
1383+
1384+
val expectedDocument = obj("items" -> array(fromString("a"), fromString("b")))
1385+
expect.same(result, expectedDocument)
1386+
}
1387+
11491388
private def inside[A, B](
11501389
a: A
11511390
)(assertPF: PartialFunction[A, Unit])(implicit loc: munit.Location) = {

0 commit comments

Comments
 (0)