Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for @nullable trait in order to support MergePatch usecases #1408

Merged
merged 30 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
65e5ed1
Add nullable support to JSON parsing
astridej Feb 20, 2024
87ca4c9
Adjust codegen code to handle Nullable
astridej Feb 20, 2024
8a64977
Add nullable sample spec and smoke tests
astridej Feb 20, 2024
8edf430
Inexplicable hint reorderings that happened on compilation
astridej Feb 20, 2024
62e4ca8
Minor refactor
astridej Feb 20, 2024
2e177fe
Scalafmt
astridej Feb 20, 2024
388b45e
Rework get message to be safer until I can test it
astridej Feb 20, 2024
67c06bc
Remove commented out code + cleanup
astridej Feb 20, 2024
41fb310
Update modules/core/src/smithy4s/Nullable.scala
astridej Feb 21, 2024
5062108
Add refinement schema to expanded .isOption logic
astridej Feb 21, 2024
8b42288
Add tests for error messages
astridej Feb 21, 2024
e4b86e9
Revert "Inexplicable hint reorderings that happened on compilation"
astridej Feb 21, 2024
119aabf
Use Option ordering for Nullable fold
astridej Feb 21, 2024
092eab6
Remove unneeded TODO comment
astridej Feb 21, 2024
e387b94
Add cats instances for Nullable
astridej Feb 21, 2024
ef98fc2
Add test and refactor for getUnlessDefault
astridej Feb 21, 2024
388c1fc
Scalafmt
astridej Feb 21, 2024
eacf966
Revert refactor of getUnlessDefault
astridej Feb 21, 2024
95abbc4
Additional tests for getUnlessDefault
astridej Feb 21, 2024
ee44174
Remove comment because it's probably misleading
astridej Feb 21, 2024
c6be356
Add support for Nullable in dynamic module
astridej Feb 22, 2024
2dbb0bb
Scalafmt
astridej Feb 22, 2024
d0926e8
Fix trait reference in dynamic module
astridej Feb 22, 2024
8344e40
Revert extra handling of default in dynamic module
astridej Feb 22, 2024
b2ae9d7
Add docs for @nullable
astridej Feb 22, 2024
67ae7ba
Scalafmt, redux
astridej Feb 23, 2024
b3f7996
Fix wrong copyright on new header
astridej Feb 23, 2024
85da837
Update changelog to include nullable
astridej Feb 23, 2024
c268f7c
Correction to the documented code
astridej Feb 23, 2024
7c44617
Update modules/docs/markdown/04-codegen/01-customisation/13-nullable-…
astridej Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Supports error responses with `@httpResponseCode` fields.

* Added support for `@nullable` on fields, to allow absent values to be handled differently from explicit null

# 0.18.8

* Fix collision avoidance algorithm to cover Scala 3 keywords
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Newtype
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.schema.Schema.bijection
import smithy4s.schema.Schema.string

object CustomErrorMessageType extends Newtype[String] {
val id: ShapeId = ShapeId("smithy4s.example", "CustomErrorMessageType")
val hints: Hints = Hints.empty
val underlyingSchema: Schema[String] = string.withId(id).addHints(hints)
implicit val schema: Schema[CustomErrorMessageType] = bijection(underlyingSchema, asBijection)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.struct

final case class ErrorCustomTypeMessage(message: Option[CustomErrorMessageType] = None) extends Smithy4sThrowable {
override def getMessage(): String = message.map(_.value).orNull
}

object ErrorCustomTypeMessage extends ShapeTag.Companion[ErrorCustomTypeMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorCustomTypeMessage")

val hints: Hints = Hints(
smithy.api.Error.SERVER.widen,
).lazily

implicit val schema: Schema[ErrorCustomTypeMessage] = struct(
CustomErrorMessageType.schema.optional[ErrorCustomTypeMessage]("message", _.message),
){
ErrorCustomTypeMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.struct

final case class ErrorCustomTypeRequiredMessage(message: CustomErrorMessageType) extends Smithy4sThrowable {
override def getMessage(): String = message.value
}

object ErrorCustomTypeRequiredMessage extends ShapeTag.Companion[ErrorCustomTypeRequiredMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorCustomTypeRequiredMessage")

val hints: Hints = Hints(
smithy.api.Error.SERVER.widen,
).lazily

implicit val schema: Schema[ErrorCustomTypeRequiredMessage] = struct(
CustomErrorMessageType.schema.required[ErrorCustomTypeRequiredMessage]("message", _.message),
){
ErrorCustomTypeRequiredMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Nullable
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.struct

final case class ErrorNullableCustomTypeMessage(message: Option[Nullable[CustomErrorMessageType]] = None) extends Smithy4sThrowable {
override def getMessage(): String = message.flatMap(_.toOption).map(_.value).orNull
}

object ErrorNullableCustomTypeMessage extends ShapeTag.Companion[ErrorNullableCustomTypeMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorNullableCustomTypeMessage")

val hints: Hints = Hints(
smithy.api.Error.SERVER.widen,
).lazily

implicit val schema: Schema[ErrorNullableCustomTypeMessage] = struct(
CustomErrorMessageType.schema.nullable.optional[ErrorNullableCustomTypeMessage]("message", _.message),
){
ErrorNullableCustomTypeMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Nullable
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.struct

final case class ErrorNullableCustomTypeRequiredMessage(message: Nullable[CustomErrorMessageType]) extends Smithy4sThrowable {
override def getMessage(): String = message.toOption.map(_.value).orNull
}

object ErrorNullableCustomTypeRequiredMessage extends ShapeTag.Companion[ErrorNullableCustomTypeRequiredMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorNullableCustomTypeRequiredMessage")

val hints: Hints = Hints(
smithy.api.Error.SERVER.widen,
).lazily

implicit val schema: Schema[ErrorNullableCustomTypeRequiredMessage] = struct(
CustomErrorMessageType.schema.nullable.required[ErrorNullableCustomTypeRequiredMessage]("message", _.message),
){
ErrorNullableCustomTypeRequiredMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Nullable
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.string
import smithy4s.schema.Schema.struct

final case class ErrorNullableMessage(message: Option[Nullable[String]] = None) extends Smithy4sThrowable {
override def getMessage(): String = message.flatMap(_.toOption).orNull
}

object ErrorNullableMessage extends ShapeTag.Companion[ErrorNullableMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorNullableMessage")

val hints: Hints = Hints(
smithy.api.Error.CLIENT.widen,
).lazily

implicit val schema: Schema[ErrorNullableMessage] = struct(
string.nullable.optional[ErrorNullableMessage]("message", _.message),
){
ErrorNullableMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Nullable
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.string
import smithy4s.schema.Schema.struct

final case class ErrorNullableRequiredMessage(message: Nullable[String]) extends Smithy4sThrowable {
override def getMessage(): String = message.toOption.orNull
}

object ErrorNullableRequiredMessage extends ShapeTag.Companion[ErrorNullableRequiredMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorNullableRequiredMessage")

val hints: Hints = Hints(
smithy.api.Error.SERVER.widen,
).lazily

implicit val schema: Schema[ErrorNullableRequiredMessage] = struct(
string.nullable.required[ErrorNullableRequiredMessage]("message", _.message),
){
ErrorNullableRequiredMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.Smithy4sThrowable
import smithy4s.schema.Schema.string
import smithy4s.schema.Schema.struct

final case class ErrorRequiredMessage(message: String) extends Smithy4sThrowable {
override def getMessage(): String = message
}

object ErrorRequiredMessage extends ShapeTag.Companion[ErrorRequiredMessage] {
val id: ShapeId = ShapeId("smithy4s.example", "ErrorRequiredMessage")

val hints: Hints = Hints(
smithy.api.Error.CLIENT.widen,
).lazily

implicit val schema: Schema[ErrorRequiredMessage] = struct(
string.required[ErrorRequiredMessage]("message", _.message),
){
ErrorRequiredMessage.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Nullable
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.schema.Schema.int
import smithy4s.schema.Schema.struct

final case class Patchable(allowExplicitNull: Option[Nullable[Int]] = None)

object Patchable extends ShapeTag.Companion[Patchable] {
val id: ShapeId = ShapeId("smithy4s.example", "Patchable")

val hints: Hints = Hints.empty

implicit val schema: Schema[Patchable] = struct(
int.nullable.optional[Patchable]("allowExplicitNull", _.allowExplicitNull),
){
Patchable.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Nullable
import smithy4s.Nullable.Null
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.schema.Schema.int
import smithy4s.schema.Schema.struct

final case class PatchableEdgeCases(required: Nullable[Int], requiredDefaultValue: Nullable[Int] = smithy4s.Nullable.Value(3), requiredDefaultNull: Nullable[Int] = Null, defaultValue: Nullable[Int] = smithy4s.Nullable.Value(5), defaultNull: Nullable[Int] = Null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great, thank you !


object PatchableEdgeCases extends ShapeTag.Companion[PatchableEdgeCases] {
val id: ShapeId = ShapeId("smithy4s.example", "PatchableEdgeCases")

val hints: Hints = Hints.empty

implicit val schema: Schema[PatchableEdgeCases] = struct(
int.nullable.required[PatchableEdgeCases]("required", _.required),
int.nullable.required[PatchableEdgeCases]("requiredDefaultValue", _.requiredDefaultValue).addHints(smithy.api.Default(smithy4s.Document.fromDouble(3.0d))),
int.nullable.required[PatchableEdgeCases]("requiredDefaultNull", _.requiredDefaultNull).addHints(smithy.api.Box(), smithy.api.Default(smithy4s.Document.nullDoc)),
int.nullable.field[PatchableEdgeCases]("defaultValue", _.defaultValue).addHints(smithy.api.Default(smithy4s.Document.fromDouble(5.0d))),
int.nullable.field[PatchableEdgeCases]("defaultNull", _.defaultNull).addHints(smithy.api.Box(), smithy.api.Default(smithy4s.Document.nullDoc)),
){
PatchableEdgeCases.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ package object example {
type NonEmptyStrings = smithy4s.example.NonEmptyStrings.Type
type TestStructurePattern = smithy4s.example.TestStructurePattern.Type
type UnicodeRegexString = smithy4s.example.UnicodeRegexString.Type
type CustomErrorMessageType = smithy4s.example.CustomErrorMessageType.Type
type UVIndex = smithy4s.example.UVIndex.Type
@deprecated(message = "N/A", since = "N/A")
type Strings = smithy4s.example.Strings.Type
Expand Down
43 changes: 41 additions & 2 deletions modules/bootstrapped/test/src/smithy4s/ErrorMessageTraitSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
package smithy4s

import munit._
import smithy4s.example.ClientError
import smithy4s.example.ServerErrorCustomMessage
import smithy4s.example._

class ErrorMessageTraitSpec extends FunSuite {

Expand All @@ -34,6 +33,46 @@ class ErrorMessageTraitSpec extends FunSuite {
)
}

test(
"Custom @errorMessage field works for various nullable/default/required combos"
) {
val customMessage = "some custom error message"
val errorsWithCustomMessage = List(
ErrorCustomTypeMessage(Some(CustomErrorMessageType(customMessage))),
ErrorCustomTypeRequiredMessage(CustomErrorMessageType(customMessage)),
ErrorNullableMessage(Some(Nullable.Value(customMessage))),
ErrorNullableRequiredMessage(Nullable.Value(customMessage)),
ErrorNullableCustomTypeMessage(
Some(Nullable.Value(CustomErrorMessageType(customMessage)))
),
ErrorNullableCustomTypeRequiredMessage(
Nullable.Value(CustomErrorMessageType(customMessage))
),
ErrorRequiredMessage(customMessage)
)
val errorsWithNullMessage = List(
ErrorCustomTypeMessage(None),
ErrorNullableMessage(None),
ErrorNullableMessage(Some(Nullable.Null)),
ErrorNullableRequiredMessage(Nullable.Null),
ErrorNullableCustomTypeMessage(None),
ErrorNullableCustomTypeMessage(Some(Nullable.Null)),
ErrorNullableCustomTypeRequiredMessage(Nullable.Null),
ServerErrorCustomMessage(None)
)

errorsWithCustomMessage.foreach(e =>
assertEquals(
e.getMessage,
customMessage,
s"Failed on ${e.getClass.getName}"
)
)
errorsWithNullMessage.foreach(e =>
assertEquals(e.getMessage, null, s"Failed on ${e.getClass.getName}")
)
}

test("Generated getMessage") {
val e = ClientError(400, "oopsy")

Expand Down
Loading