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 default support in several locations #358

Merged
merged 11 commits into from
Aug 18, 2022
10 changes: 7 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ lazy val core = projectMatrix
(ThisBuild / baseDirectory).value / "sampleSpecs" / "errors.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "example.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "adtMember.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "enums.smithy"
(ThisBuild / baseDirectory).value / "sampleSpecs" / "enums.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "defaults.smithy"
),
(Test / sourceGenerators) := Seq(genSmithyScala(Test).taskValue),
testFrameworks += new TestFramework("weaver.framework.CatsEffect"),
Expand Down Expand Up @@ -300,6 +301,7 @@ lazy val codegen = projectMatrix
buildInfoKeys := Seq[BuildInfoKey](version, scalaBinaryVersion),
buildInfoPackage := "smithy4s.codegen",
isCE3 := true,
resolvers += Resolver.mavenLocal,
libraryDependencies ++= Seq(
Dependencies.Cats.core.value,
Dependencies.Smithy.model,
Expand Down Expand Up @@ -390,6 +392,7 @@ lazy val protocol = projectMatrix
.filterNot { case (file, path) =>
path.equalsIgnoreCase("META-INF/smithy/manifest")
},
resolvers += Resolver.mavenLocal,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will remove when changing to use official smithy 2 release

libraryDependencies += Dependencies.Smithy.model,
javacOptions ++= Seq("--release", "8")
)
Expand Down Expand Up @@ -620,7 +623,8 @@ lazy val example = projectMatrix
(ThisBuild / baseDirectory).value / "sampleSpecs" / "brandscommon.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "refined.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "enums.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "mixins.smithy"
(ThisBuild / baseDirectory).value / "sampleSpecs" / "mixins.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "defaults.smithy"
),
Compile / resourceDirectory := (ThisBuild / baseDirectory).value / "modules" / "example" / "resources",
isCE3 := true,
Expand Down Expand Up @@ -673,7 +677,7 @@ lazy val Dependencies = new {
)

val Smithy = new {
val smithyVersion = "1.21.0-rc1"
val smithyVersion = "1.21.0-SNAPSHOT"
val model = "software.amazon.smithy" % "smithy-model" % smithyVersion
val build = "software.amazon.smithy" % "smithy-build" % smithyVersion
val awsTraits =
Expand Down
1 change: 1 addition & 0 deletions modules/codegen/src/smithy4s/codegen/SmithyToIR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) {

override def listShape(x: ListShape): Option[Decl] = getDefault(x)

@annotation.nowarn("msg=class SetShape in package shapes is deprecated")
override def setShape(x: SetShape): Option[Decl] = getDefault(x)

override def mapShape(x: MapShape): Option[Decl] = getDefault(x)
Expand Down
4 changes: 3 additions & 1 deletion modules/core/src/smithy4s/http/internals/MetaDecode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ private[http] sealed abstract class MetaDecode[+A] {
binding: HttpBinding,
fieldName: String,
optional: Boolean,
reservedQueries: Set[String]
reservedQueries: Set[String],
maybeDefault: Option[Any]
): (Metadata, PutField) => Unit = {
// format: off

Expand All @@ -59,6 +60,7 @@ private[http] sealed abstract class MetaDecode[+A] {
m(metadata).get(key) match {
case Some(value) if optional => process(value, fieldName, putField.putSome(_, _))
case Some(value) => process(value, fieldName, putField.putRequired(_, _))
case None if optional && maybeDefault.isDefined => putField.putSome(fieldName, maybeDefault.get)
case None if optional => putField.putNone(fieldName)
case None => throw new MetadataError.NotFound(fieldName, binding)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,20 @@ private[http] class SchemaVisitorMetadataReader()
val schema = field.instance
val label = field.label
val fieldHints = field.hints
val maybeDefault = field.getDefault.flatMap(d =>
Document.Decoder.fromSchema(field.instance).decode(d).toOption
)
HttpBinding.fromHints(label, fieldHints, hints).map { binding =>
val decoder: MetaDecode[_] =
self(schema.addHints(Hints(binding)))
val update = decoder
.updateMetadata(binding, label, field.isOptional, reservedQueries)
.updateMetadata(
binding,
label,
field.isOptional,
reservedQueries,
maybeDefault
)
FieldDecode(label, binding, update)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import java.util.Base64
import java.util.UUID

import smithy.api.JsonName
import smithy.api.Default
import smithy.api.TimestampFormat
import smithy.api.TimestampFormat.DATE_TIME
import smithy.api.TimestampFormat.EPOCH_SECONDS
Expand Down Expand Up @@ -303,6 +304,8 @@ object DocumentDecoderSchemaVisitor extends SchemaVisitor[DocumentDecoder] {
): DocumentDecoder[S] = {
def jsonLabel[A](field: Field[Schema, S, A]): String =
field.instance.hints.get(JsonName).map(_.value).getOrElse(field.label)
def getDefault[A](field: Field[Schema, S, A]): Option[Document] =
field.instance.hints.get(Default).map(_.value)

def fieldDecoder[A](
field: Field[Schema, S, A]
Expand All @@ -313,6 +316,8 @@ object DocumentDecoderSchemaVisitor extends SchemaVisitor[DocumentDecoder] {
) => Unit = {
val jLabel = jsonLabel(field)

val maybeDefault = getDefault(field)

if (field.isOptional) {
(
pp: List[PayloadPath.Segment],
Expand All @@ -324,6 +329,8 @@ object DocumentDecoderSchemaVisitor extends SchemaVisitor[DocumentDecoder] {
.get(jLabel) match {
case Some(document) =>
buffer(Some(apply(field.instance)(path, document)))
case None if maybeDefault.isDefined =>
buffer(Some(apply(field.instance)(path, maybeDefault.get)))
case None => buffer(None)
}
} else {
Expand Down
3 changes: 3 additions & 0 deletions modules/core/src/smithy4s/schema/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ object Field {
}
}

def getDefault: Option[Document] =
field.instance.hints.get(smithy.api.Default).map(_.value)

}

}
45 changes: 45 additions & 0 deletions modules/core/test/src/smithy4s/DocumentSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package smithy4s

import smithy.api.JsonName
import smithy.api.Default
import smithy4s.api.Discriminated
import smithy4s.example.IntList
import munit._
Expand Down Expand Up @@ -184,4 +185,48 @@ class DocumentSpec() extends FunSuite {
assertEquals(roundTripped, Right(faceCard))
}

case class DefTest(int: Option[Int], str: Option[String])
implicit val withDefaultsSchema: Schema[DefTest] = {
val i = int
.optional[DefTest]("int", _.int)
.addHints(Default(Document.fromInt(11)))
val s =
string
.optional[DefTest]("str", _.str)
.addHints(Default(Document.fromString("test")))
struct(i, s)(DefTest.apply)
}

test("defaults should be applied when fields missing") {
val defTest = DefTest(None, None)

val document = Document.encode(defTest)
import Document._
val expectedEncoded = obj()

val expectedDecoded = DefTest(Some(11), Some("test"))

val fromEmpty = Document.decode[DefTest](obj())

expect(document == expectedEncoded)
expect(fromEmpty == Right(expectedDecoded))
}

test("defaults should not be applied when field is provided") {
val defTest = DefTest(Some(12), Some("test2"))

val document = Document.encode(defTest)
import Document._
val expectedDocument =
obj(
"int" -> fromInt(12),
"str" -> fromString("test2")
)

val roundTripped = Document.decode[DefTest](document)

expect(document == expectedDocument)
expect(roundTripped == Right(defTest))
}

}
1 change: 0 additions & 1 deletion modules/core/test/src/smithy4s/ShapeIdHintsSmokeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class ShapeIdHintsSmokeSpec() extends munit.FunSuite {

test("newtypes contain ShapeId in hints") {
val shapeIds = example.CityId.schema.compile(TestCompiler)
println(s"YEAH $shapeIds")
expect(
shapeIds.contains(
ShapeId(
Expand Down
52 changes: 52 additions & 0 deletions modules/core/test/src/smithy4s/http/internals/MetadataSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import cats.syntax.all._
import smithy4s.Schema
import smithy4s.Timestamp
import smithy4s.example.Headers
import smithy4s.example.HeadersWithDefaults
import smithy4s.example.PathParams
import smithy4s.example.Queries
import smithy4s.example.QueriesWithDefaults
import smithy4s.example.ValidationChecks
import smithy4s.http.CaseInsensitive
import smithy4s.http.HttpBinding
Expand All @@ -37,6 +39,10 @@ class MetadataSpec() extends FunSuite {
Queries.schema.addHints(InputOutput.Input.widen)
implicit val headersSchema: Schema[Headers] =
Headers.schema.addHints(InputOutput.Input.widen)
implicit val queriesDefaultSchema: Schema[QueriesWithDefaults] =
QueriesWithDefaults.schema.addHints(InputOutput.Input.widen)
implicit val headersDefaultSchema: Schema[HeadersWithDefaults] =
HeadersWithDefaults.schema.addHints(InputOutput.Input.widen)
implicit val pathParamsSchema: Schema[PathParams] =
PathParams.schema.addHints(InputOutput.Input.widen)
implicit val validationChecksSchema: Schema[ValidationChecks] =
Expand All @@ -59,6 +65,24 @@ class MetadataSpec() extends FunSuite {
checkRoundTripTotal(a, expectedEncoding)
}

def checkRoundTripDefault[A](a: A, expectedDecoded: A)(implicit
s: Schema[A],
loc: Location
): Unit = {
val encoded = Metadata.encode(a)
val result = Metadata
.decodePartial[A](encoded)
.left
.map(_.getMessage())
.flatMap { partial =>
s.compile(FromMetadataSchemaVisitor).read(partial.decoded.toMap)
}
val expectedEncoding = Metadata.empty
expect.same(encoded, expectedEncoding)
expect.same(result, Right(expectedDecoded))
checkRoundTripTotalDefault(a, expectedDecoded)
}

def checkRoundTripError[A](a: A, expectedEncoding: Metadata, message: String)(
implicit
s: Schema[A],
Expand Down Expand Up @@ -113,6 +137,22 @@ class MetadataSpec() extends FunSuite {
expect.same(result, Some(Right(a)))
}

def checkRoundTripTotalDefault[A](a: A, expectedDecoded: A)(implicit
s: Schema[A],
loc: Location
): Unit = {
val encoded = Metadata.encode(a)
val result = Metadata
.decodeTotal[A](encoded)
.map(
_.left
.map(_.getMessage())
)
val expectedEncoding = Metadata.empty
expect.same(encoded, expectedEncoding)
expect.same(result, Some(Right(expectedDecoded)))
}

val epochString = "1970-01-01T00:00:00Z"

val constraintMessage1 =
Expand All @@ -131,6 +171,12 @@ class MetadataSpec() extends FunSuite {
checkRoundTrip(queries, expected)
}

test("String query parameter with default") {
val queries = QueriesWithDefaults(dflt = None)
val expectedDecoded = QueriesWithDefaults(dflt = Some("test"))
checkRoundTripDefault(queries, expectedDecoded)
}

test("String length constraint violation") {
val string = "1" * 11
val queries = ValidationChecks(str = Some(string))
Expand Down Expand Up @@ -235,6 +281,12 @@ class MetadataSpec() extends FunSuite {
checkRoundTrip(headers, expected)
}

test("String header with default") {
val headers = HeadersWithDefaults(dflt = None)
val expected = HeadersWithDefaults(dflt = Some("test"))
checkRoundTripDefault(headers, expected)
}

test("Integer header") {
val headers = Headers(int = Some(123))
val expected = Metadata.empty.addHeader("int", "123")
Expand Down
2 changes: 1 addition & 1 deletion modules/example/src/smithy4s/example/AgeFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object AgeFormat extends smithy4s.ShapeTag.Companion[AgeFormat] {
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "ageFormat")

val hints : smithy4s.Hints = smithy4s.Hints(
smithy.api.Trait(Some("integer"), None, None),
smithy.api.Trait(Some("integer"), None, None, None),
)

implicit val schema: smithy4s.Schema[AgeFormat] = constant(AgeFormat()).withId(id).addHints(hints)
Expand Down
2 changes: 1 addition & 1 deletion modules/example/src/smithy4s/example/ArbitraryData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import smithy4s.schema.Schema._
object ArbitraryData extends Newtype[Document] {
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "arbitraryData")
val hints : smithy4s.Hints = smithy4s.Hints(
smithy.api.Trait(None, None, None),
smithy.api.Trait(None, None, None, None),
)
val underlyingSchema : smithy4s.Schema[Document] = document.withId(id).addHints(hints)
implicit val schema : smithy4s.Schema[ArbitraryData] = bijection(underlyingSchema, asBijection)
Expand Down
18 changes: 18 additions & 0 deletions modules/example/src/smithy4s/example/DefaultTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package smithy4s.example

import smithy4s.schema.Schema._

case class DefaultTest(one: Option[Int] = None, two: Option[String] = None, three: Option[List[String]] = None)
object DefaultTest extends smithy4s.ShapeTag.Companion[DefaultTest] {
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "DefaultTest")

val hints : smithy4s.Hints = smithy4s.Hints.empty

implicit val schema: smithy4s.Schema[DefaultTest] = struct(
int.optional[DefaultTest]("one", _.one).addHints(smithy.api.Default(smithy4s.Document.fromDouble(1.0))),
string.optional[DefaultTest]("two", _.two).addHints(smithy.api.Default(smithy4s.Document.fromString("test"))),
StringList.underlyingSchema.optional[DefaultTest]("three", _.three).addHints(smithy.api.Default(smithy4s.Document.array())),
){
DefaultTest.apply
}.withId(id).addHints(hints)
}
2 changes: 1 addition & 1 deletion modules/example/src/smithy4s/example/FancyListFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object FancyListFormat extends smithy4s.ShapeTag.Companion[FancyListFormat] {
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "fancyListFormat")

val hints : smithy4s.Hints = smithy4s.Hints(
smithy.api.Trait(Some("list:test(> member > string)"), None, None),
smithy.api.Trait(Some("list:test(> member > string)"), None, None, None),
)

implicit val schema: smithy4s.Schema[FancyListFormat] = constant(FancyListFormat()).withId(id).addHints(hints)
Expand Down
2 changes: 1 addition & 1 deletion modules/example/src/smithy4s/example/NameFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object NameFormat extends smithy4s.ShapeTag.Companion[NameFormat] {
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "nameFormat")

val hints : smithy4s.Hints = smithy4s.Hints(
smithy.api.Trait(Some("string"), None, None),
smithy.api.Trait(Some("string"), None, None, None),
)

implicit val schema: smithy4s.Schema[NameFormat] = constant(NameFormat()).withId(id).addHints(hints)
Expand Down
4 changes: 2 additions & 2 deletions modules/example/src/smithy4s/example/StreamedObjects.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ object StreamedObjectsGen extends smithy4s.Service[StreamedObjectsGen, StreamedO
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "PutStreamedObject")
val input: smithy4s.Schema[PutStreamedObjectInput] = PutStreamedObjectInput.schema.addHints(smithy4s.internals.InputOutput.Input.widen)
val output: smithy4s.Schema[Unit] = unit.addHints(smithy4s.internals.InputOutput.Output.widen)
val streamedInput : smithy4s.StreamingSchema[StreamedBlob] = smithy4s.StreamingSchema("PutStreamedObjectInput", StreamedBlob.schema)
val streamedInput : smithy4s.StreamingSchema[StreamedBlob] = smithy4s.StreamingSchema("PutStreamedObjectInput", StreamedBlob.schema.addHints(smithy.api.Default(smithy4s.Document.fromString(""))))
val streamedOutput : smithy4s.StreamingSchema[Nothing] = smithy4s.StreamingSchema.nothing
val hints : smithy4s.Hints = smithy4s.Hints.empty
def wrap(input: PutStreamedObjectInput) = PutStreamedObject(input)
Expand All @@ -66,7 +66,7 @@ object StreamedObjectsGen extends smithy4s.Service[StreamedObjectsGen, StreamedO
val input: smithy4s.Schema[GetStreamedObjectInput] = GetStreamedObjectInput.schema.addHints(smithy4s.internals.InputOutput.Input.widen)
val output: smithy4s.Schema[GetStreamedObjectOutput] = GetStreamedObjectOutput.schema.addHints(smithy4s.internals.InputOutput.Output.widen)
val streamedInput : smithy4s.StreamingSchema[Nothing] = smithy4s.StreamingSchema.nothing
val streamedOutput : smithy4s.StreamingSchema[StreamedBlob] = smithy4s.StreamingSchema("GetStreamedObjectOutput", StreamedBlob.schema)
val streamedOutput : smithy4s.StreamingSchema[StreamedBlob] = smithy4s.StreamingSchema("GetStreamedObjectOutput", StreamedBlob.schema.addHints(smithy.api.Default(smithy4s.Document.fromString(""))))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Afaik AWS automatically adds this default since the blob shape used to be boxed and now isn't, this would go away if we updated streaming.smithy to be $version 2.0

Copy link
Contributor

Choose a reason for hiding this comment

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

interesting

val hints : smithy4s.Hints = smithy4s.Hints.empty
def wrap(input: GetStreamedObjectInput) = GetStreamedObject(input)
}
Expand Down
11 changes: 11 additions & 0 deletions modules/example/src/smithy4s/example/StringList.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package smithy4s.example

import smithy4s.Newtype
import smithy4s.schema.Schema._

object StringList extends Newtype[List[String]] {
val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "StringList")
val hints : smithy4s.Hints = smithy4s.Hints.empty
val underlyingSchema : smithy4s.Schema[List[String]] = list(string).withId(id).addHints(hints)
implicit val schema : smithy4s.Schema[StringList] = bijection(underlyingSchema, asBijection)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ object StructureWithRefinedTypes extends smithy4s.ShapeTag.Companion[StructureWi
val hints : smithy4s.Hints = smithy4s.Hints.empty

implicit val schema: smithy4s.Schema[StructureWithRefinedTypes] = struct(
Age.schema.optional[StructureWithRefinedTypes]("age", _.age).addHints(smithy.api.Default()),
PersonAge.schema.optional[StructureWithRefinedTypes]("personAge", _.personAge).addHints(smithy.api.Default()),
Age.schema.optional[StructureWithRefinedTypes]("age", _.age).addHints(smithy.api.Default(smithy4s.Document.fromDouble(0.0))),
PersonAge.schema.optional[StructureWithRefinedTypes]("personAge", _.personAge).addHints(smithy.api.Default(smithy4s.Document.fromDouble(0.0))),
FancyList.schema.optional[StructureWithRefinedTypes]("fancyList", _.fancyList),
UnwrappedFancyList.underlyingSchema.optional[StructureWithRefinedTypes]("unwrappedFancyList", _.unwrappedFancyList),
Name.schema.optional[StructureWithRefinedTypes]("name", _.name),
Expand Down
Loading