Skip to content

Commit

Permalink
add tests and docs for decoding of different field types
Browse files Browse the repository at this point in the history
  • Loading branch information
lewisjkl committed Jan 19, 2025
1 parent 081bb3e commit a8a6bcd
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 73 deletions.
149 changes: 114 additions & 35 deletions modules/bootstrapped/test/src/smithy4s/DocumentSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -953,47 +953,126 @@ class DocumentSpec() extends FunSuite {
assertEquals(doc, expected)
}

test(
"Required nullable field with NO default should NOT infer null when value missing"
) {
case class Foo(str: Nullable[String])
implicit val fieldSchema: Schema[Foo] =
Schema
.struct[Foo](
Schema.string.nullable
.required[Foo]("str", _.str)
)(Foo.apply)
test("combinations of required, nullable, and null default") {
testFieldCombination(true, true, true)
testFieldCombination(false, true, true)
testFieldCombination(false, false, true)
testFieldCombination(false, false, false)
testFieldCombination(true, false, false)
testFieldCombination(true, true, false)
testFieldCombination(true, false, true)
testFieldCombination(false, true, false)
}

val result = Document.decode[Foo](Document.DObject(Map.empty))
expect.same(
result,
Left(
smithy4s.codecs.PayloadError(
smithy4s.codecs.PayloadPath.parse(".str"),
"str",
"Required field not found"
private def testFieldCombination(
required: Boolean,
nullable: Boolean,
nullDefault: Boolean
)(implicit loc: munit.Location): Unit = {
val toDecode = Document.DObject(Map.empty)
val hints =
if (nullDefault) Hints(smithy.api.Default(Document.DNull))
else Hints.empty
// scalafmt: { maxColumn: 120 }
if (!required && nullDefault) nonRequiredWithDefault(nullable, hints, toDecode)
else if (required && nullable) requiredNullable(nullDefault, hints, toDecode)
else if (!required && nullable && !nullDefault) nonRequiredNullable(hints, toDecode)
else if (required) requiredNonNullable(nullDefault, hints, toDecode)
else if (!nullDefault) nonRequiredNonNullable(hints, toDecode)
}

def nonRequiredWithDefault(
nullable: Boolean,
hints: Hints,
toDecode: Document
)(implicit loc: munit.Location): Unit = {
if (nullable) {
case class Foo(f: Nullable[String])
implicit val schema: Schema[Foo] =
Schema.struct(Schema.string.nullable.field[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = Document.decode[Foo](toDecode)
// required = false, nullable = true, nullDefault = true
expect.same(result.toOption.get, Foo(Nullable.Null))
} else {
case class Foo(f: String)
implicit val schema: Schema[Foo] =
Schema.struct(Schema.string.field[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = Document.decode[Foo](toDecode)
// required = false, nullable = false, nullDefault = true
expect.same(result.toOption.get, Foo(""))
}
}

def requiredNullable(
nullDefault: Boolean,
hints: Hints,
toDecode: Document
)(implicit loc: munit.Location): Unit = {
case class Foo(f: Nullable[String])
implicit val schema: Schema[Foo] =
Schema.struct(
Schema.string.nullable.required[Foo]("f", _.f).addHints(hints)
)(
Foo.apply
)
)
val result = Document.decode[Foo](toDecode)
if (nullDefault)
// required = true, nullable = true, nullDefault = true
expect.same(result.toOption.get, Foo(Nullable.Null))
else
// required = true, nullable = true, nullDefault = false
expect(result.isLeft)
}

test(
"Required nullable field WITH null default SHOULD infer null when value missing"
) {
case class Foo(str: Nullable[String])
implicit val fieldSchema: Schema[Foo] =
Schema
.struct[Foo](
Schema.string.nullable
.required[Foo]("str", _.str)
.addHints(smithy.api.Default(Document.DNull))
)(Foo.apply)
def nonRequiredNullable(
hints: Hints,
toDecode: Document
)(implicit loc: munit.Location): Unit = {
case class Foo(f: Option[Nullable[String]])
implicit val schema =
Schema.struct(
Schema.string.nullable.optional[Foo]("f", _.f).addHints(hints)
)(
Foo.apply
)
val result = Document.decode[Foo](toDecode)
// required = false, nullable = true, nullDefault = false
expect.same(result.toOption.get, Foo(None))
}

val result = Document.decode[Foo](Document.DObject(Map.empty))
expect.same(
result,
Right(Foo(Nullable.Null))
)
def requiredNonNullable(
nullDefault: Boolean,
hints: Hints,
toDecode: Document
)(implicit loc: munit.Location): Unit = {
case class Foo(f: String)
implicit val schema: Schema[Foo] =
Schema.struct(Schema.string.required[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = Document.decode[Foo](toDecode)
// required = true, nullable = false, nullDefault = true
if (nullDefault) expect.same(result.toOption.get, Foo(""))
// required = true, nullable = false, nullDefault = false
else expect(result.isLeft)
}

def nonRequiredNonNullable(
hints: Hints,
toDecode: Document
)(implicit loc: munit.Location): Unit = {
case class Foo(f: Option[String])
implicit val schema =
Schema.struct(Schema.string.optional[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = Document.decode[Foo](toDecode)
// required = false, nullable = false, nullDefault = false
expect.same(result.toOption.get, Foo(None))
}

test(
Expand Down
23 changes: 23 additions & 0 deletions modules/docs/markdown/04-codegen/03-default-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,26 @@ Here the default for the field `one` will be assumed to be an empty string (`""`
| resource | N/A |

Not every shape type has a corresponding zero value. For example, there is no reasonable zero value for a structure or a union type. As such, they will not have a zero value set even if they are marked with a null default trait.

## Decoding and defaults

The following table shows different scenarios for decoding of a structure named `Foo` with a single field `s`. The type of the field will differ depending on the scenario. However, the input for each scenario is the same: an empty JSON Object (`{}`). We are using JSON to show this behavior (based on the smithy4s json module), but the same is true of how smithy4s decodes `Document` with `Document.DObject(Map.empty)` as an input.

| Required | Nullable | Null Default | Scala Representation | Input: {} |
|----------|----------|--------------|------------------------------------|------------------------------|
| false | true | true | `Foo(s: Nullable[String])` | Foo(Null) |
| false | true | false | `Foo(s: Option[Nullable[String]])` | Foo(None) |
| false | false | true | `Foo(s: String)` | Foo("") |
| false | false | false | `Foo(s: Option[String])` | Foo(None) |
| true | false | false | `Foo(s: String)` | Missing required field error |
| true | false | true | `Foo(s: String)` | Foo("") |
| true | true | false | `Foo(s: Nullable[String])` | Missing required field error |
| true | true | true | `Foo(s: Nullable[String])` | Foo(Null) |

#### Key for Table Above

* Required - True if the field is required, false if not (using `smithy.api#required` trait)
* Nullable - True if the field is nullable, false if not (using `alloy#nullable` trait)
* Null Default - True if the field has a default value of null, false if it has no default (using `smithy.api#default` trait)
* Scala Representation - Shows what type is generated for this schema by smithy4s
* Input: {} - Shows the result of what smithy4s will return when decoding the input of an empty JSON object (`{}`)
155 changes: 117 additions & 38 deletions modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -837,47 +837,126 @@ class SchemaVisitorJCodecTests() extends FunSuite {
expect.same(result, json)
}

test(
"Required nullable field with NO default should NOT infer null when value missing"
) {
case class Foo(str: Nullable[String])
implicit val fieldSchema: Schema[Foo] =
Schema
.struct[Foo](
Schema.string.nullable
.required[Foo]("str", _.str)
)(Foo.apply)

val result = util.Try(readFromString[Foo]("{}")).toEither
expect.same(
result,
Left(
PayloadError(
PayloadPath.parse(".str"),
"str",
"Missing required field"
test("combinations of required, nullable, and null default") {
testFieldCombination(true, true, true)
testFieldCombination(false, true, true)
testFieldCombination(false, false, true)
testFieldCombination(false, false, false)
testFieldCombination(true, false, false)
testFieldCombination(true, true, false)
testFieldCombination(true, false, true)
testFieldCombination(false, true, false)
}

private def testFieldCombination(
required: Boolean,
nullable: Boolean,
nullDefault: Boolean
)(implicit loc: munit.Location): Unit = {
val toDecode = "{}"
val hints =
if (nullDefault) Hints(smithy.api.Default(Document.DNull))
else Hints.empty
// scalafmt: { maxColumn: 120 }
if (!required && nullDefault) nonRequiredWithDefault(nullable, hints, toDecode)
else if (required && nullable) requiredNullable(nullDefault, hints, toDecode)
else if (!required && nullable && !nullDefault) nonRequiredNullable(hints, toDecode)
else if (required) requiredNonNullable(nullDefault, hints, toDecode)
else if (!nullDefault) nonRequiredNonNullable(hints, toDecode)
}

def nonRequiredWithDefault(
nullable: Boolean,
hints: Hints,
toDecode: String
)(implicit loc: munit.Location): Unit = {
if (nullable) {
case class Foo(f: Nullable[String])
implicit val schema: Schema[Foo] =
Schema.struct(Schema.string.nullable.field[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
)
)
val result = util.Try(readFromString[Foo](toDecode))
// required = false, nullable = true, nullDefault = true
expect.same(result.get, Foo(Nullable.Null))
} else {
case class Foo(f: String)
implicit val schema: Schema[Foo] =
Schema.struct(Schema.string.field[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = util.Try(readFromString[Foo](toDecode))
// required = false, nullable = false, nullDefault = true
expect.same(result.get, Foo(""))
}
}

test(
"Required nullable field WITH null default SHOULD infer null when value missing"
) {
case class Foo(str: Nullable[String])
implicit val fieldSchema: Schema[Foo] =
Schema
.struct[Foo](
Schema.string.nullable
.required[Foo]("str", _.str)
.addHints(smithy.api.Default(Document.DNull))
)(Foo.apply)

val result = readFromString[Foo]("{}")
expect.same(
result,
Foo(Nullable.Null)
)
def requiredNullable(
nullDefault: Boolean,
hints: Hints,
toDecode: String
)(implicit loc: munit.Location): Unit = {
case class Foo(f: Nullable[String])
implicit val schema: Schema[Foo] =
Schema.struct(
Schema.string.nullable.required[Foo]("f", _.f).addHints(hints)
)(
Foo.apply
)
val result = util.Try(readFromString[Foo](toDecode))
if (nullDefault)
// required = true, nullable = true, nullDefault = true
expect.same(result.get, Foo(Nullable.Null))
else
// required = true, nullable = true, nullDefault = false
expect(result.isFailure)
}

def nonRequiredNullable(
hints: Hints,
toDecode: String
)(implicit loc: munit.Location): Unit = {
case class Foo(f: Option[Nullable[String]])
implicit val schema =
Schema.struct(
Schema.string.nullable.optional[Foo]("f", _.f).addHints(hints)
)(
Foo.apply
)
val result = readFromString[Foo](toDecode)
// required = false, nullable = true, nullDefault = false
expect.same(result, Foo(None))
}

def requiredNonNullable(
nullDefault: Boolean,
hints: Hints,
toDecode: String
)(implicit loc: munit.Location): Unit = {
case class Foo(f: String)
implicit val schema: Schema[Foo] =
Schema.struct(Schema.string.required[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = util.Try(readFromString[Foo](toDecode))
// required = true, nullable = false, nullDefault = true
if (nullDefault) expect.same(result.get, Foo(""))
// required = true, nullable = false, nullDefault = false
else expect(result.isFailure)
}

def nonRequiredNonNullable(
hints: Hints,
toDecode: String
)(implicit loc: munit.Location): Unit = {
case class Foo(f: Option[String])
implicit val schema =
Schema.struct(Schema.string.optional[Foo]("f", _.f).addHints(hints))(
Foo.apply
)
val result = readFromString[Foo](toDecode)
// required = false, nullable = false, nullDefault = false
expect.same(result, Foo(None))
}

test(
Expand Down

0 comments on commit a8a6bcd

Please sign in to comment.