Skip to content

Commit

Permalink
Improved default handling (#1315)
Browse files Browse the repository at this point in the history
* WIP

* Skip none values

* Fix compilation for scala 2.12

* Update docs + add new param to document encoder visitor

* PR comments

* Update test

* PR comments - move tests and improve UX

* Update the changelog

* Regenerate files
  • Loading branch information
msosnicki committed Jan 19, 2024
1 parent 84bddcb commit 2a5007a
Show file tree
Hide file tree
Showing 15 changed files with 801 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ These are now fixed in [#1344](https://github.com/disneystreaming/smithy4s/pull/

* In some concurrent scenarios, especially those of concurrent initialization of objects (e.g. tests), your application would previously be at risk of deadlocking due to [#537](https://github.com/disneystreaming/smithy4s/issues/537). This is now fixed by suspending evaluation of hints in companion objects using the `.lazily` construct: see [#1326](https://github.com/disneystreaming/smithy4s/pull/1326).

* Allow to configure how the default values (and nulls for optional fields) are rendered. Fixed in [#1315](https://github.com/disneystreaming/smithy4s/pull/1315)

# 0.18.5

* When encoding to `application/x-www-form-urlencoded`, omit optional fields set to the field's default value.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"openapi": "3.0.2",
"info": {
"title": "ServiceWithNullsAndDefaults",
"version": "1.0.0"
},
"paths": {
"/operation/{requiredLabel}": {
"post": {
"operationId": "Operation",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OperationRequestContent"
}
}
},
"required": true
},
"parameters": [
{
"name": "requiredLabel",
"in": "path",
"schema": {
"type": "string",
"default": "required-label-with-default"
},
"required": true
},
{
"name": "optional-query",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "optional-query-with-default",
"in": "query",
"schema": {
"type": "string",
"default": "optional-query-with-default"
}
},
{
"name": "required-query-with-default",
"in": "query",
"schema": {
"type": "string",
"default": "required-query-with-default"
}
},
{
"name": "optional-header",
"in": "header",
"schema": {
"type": "string"
}
},
{
"name": "optional-header-with-default",
"in": "header",
"schema": {
"type": "string",
"default": "optional-header-with-default"
}
},
{
"name": "required-header-with-default",
"in": "header",
"schema": {
"type": "string",
"default": "required-header-with-default"
},
"required": true
}
],
"responses": {
"200": {
"description": "Operation 200 response",
"headers": {
"optional-header": {
"schema": {
"type": "string"
}
},
"optional-header-with-default": {
"schema": {
"type": "string",
"default": "optional-header-with-default"
}
},
"required-header-with-default": {
"schema": {
"type": "string",
"default": "required-header-with-default"
},
"required": true
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OperationResponseContent"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"OperationRequestContent": {
"type": "object",
"properties": {
"optional": {
"type": "string"
},
"optionalWithDefault": {
"type": "string",
"default": "optional-default"
},
"requiredWithDefault": {
"type": "string",
"default": "required-default"
}
},
"required": [
"requiredWithDefault"
]
},
"OperationResponseContent": {
"type": "object",
"properties": {
"optional": {
"type": "string"
},
"optionalWithDefault": {
"type": "string",
"default": "optional-default"
},
"requiredWithDefault": {
"type": "string",
"default": "required-default"
}
},
"required": [
"requiredWithDefault"
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package smithy4s.example

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

final case class OperationInput(optionalWithDefault: String = "optional-default", requiredLabel: String = "required-label-with-default", requiredWithDefault: String = "required-default", optionalHeaderWithDefault: String = "optional-header-with-default", requiredHeaderWithDefault: String = "required-header-with-default", optionalQueryWithDefault: String = "optional-query-with-default", requiredQueryWithDefault: String = "required-query-with-default", optional: Option[String] = None, optionalHeader: Option[String] = None, optionalQuery: Option[String] = None)

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

val hints: Hints = Hints(
smithy.api.Input(),
).lazily

implicit val schema: Schema[OperationInput] = struct(
string.field[OperationInput]("optionalWithDefault", _.optionalWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-default"))),
string.required[OperationInput]("requiredLabel", _.requiredLabel).addHints(smithy.api.Default(smithy4s.Document.fromString("required-label-with-default")), smithy.api.HttpLabel()),
string.required[OperationInput]("requiredWithDefault", _.requiredWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-default"))),
string.field[OperationInput]("optionalHeaderWithDefault", _.optionalHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-header-with-default")), smithy.api.HttpHeader("optional-header-with-default")),
string.required[OperationInput]("requiredHeaderWithDefault", _.requiredHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-header-with-default")), smithy.api.HttpHeader("required-header-with-default")),
string.field[OperationInput]("optionalQueryWithDefault", _.optionalQueryWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-query-with-default")), smithy.api.HttpQuery("optional-query-with-default")),
string.field[OperationInput]("requiredQueryWithDefault", _.requiredQueryWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-query-with-default")), smithy.api.HttpQuery("required-query-with-default")),
string.optional[OperationInput]("optional", _.optional),
string.optional[OperationInput]("optionalHeader", _.optionalHeader).addHints(smithy.api.HttpHeader("optional-header")),
string.optional[OperationInput]("optionalQuery", _.optionalQuery).addHints(smithy.api.HttpQuery("optional-query")),
){
OperationInput.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package smithy4s.example

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

final case class OperationOutput(optionalWithDefault: String = "optional-default", requiredWithDefault: String = "required-default", optionalHeaderWithDefault: String = "optional-header-with-default", requiredHeaderWithDefault: String = "required-header-with-default", optional: Option[String] = None, optionalHeader: Option[String] = None)

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

val hints: Hints = Hints(
smithy.api.Output(),
).lazily

implicit val schema: Schema[OperationOutput] = struct(
string.field[OperationOutput]("optionalWithDefault", _.optionalWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-default"))),
string.required[OperationOutput]("requiredWithDefault", _.requiredWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-default"))),
string.field[OperationOutput]("optionalHeaderWithDefault", _.optionalHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("optional-header-with-default")), smithy.api.HttpHeader("optional-header-with-default")),
string.required[OperationOutput]("requiredHeaderWithDefault", _.requiredHeaderWithDefault).addHints(smithy.api.Default(smithy4s.Document.fromString("required-header-with-default")), smithy.api.HttpHeader("required-header-with-default")),
string.optional[OperationOutput]("optional", _.optional),
string.optional[OperationOutput]("optionalHeader", _.optionalHeader).addHints(smithy.api.HttpHeader("optional-header")),
){
OperationOutput.apply
}.withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package smithy4s.example

import smithy4s.Endpoint
import smithy4s.Hints
import smithy4s.Schema
import smithy4s.Service
import smithy4s.ShapeId
import smithy4s.Transformation
import smithy4s.kinds.PolyFunction5
import smithy4s.kinds.toPolyFunction5.const5
import smithy4s.schema.OperationSchema

trait ServiceWithNullsAndDefaultsGen[F[_, _, _, _, _]] {
self =>

def operation(input: OperationInput): F[OperationInput, Nothing, OperationOutput, Nothing, Nothing]

def transform: Transformation.PartiallyApplied[ServiceWithNullsAndDefaultsGen[F]] = Transformation.of[ServiceWithNullsAndDefaultsGen[F]](this)
}

object ServiceWithNullsAndDefaultsGen extends Service.Mixin[ServiceWithNullsAndDefaultsGen, ServiceWithNullsAndDefaultsOperation] {

val id: ShapeId = ShapeId("smithy4s.example", "ServiceWithNullsAndDefaults")
val version: String = "1.0.0"

val hints: Hints = Hints(
alloy.SimpleRestJson(),
).lazily

def apply[F[_]](implicit F: Impl[F]): F.type = F

object ErrorAware {
def apply[F[_, _]](implicit F: ErrorAware[F]): F.type = F
type Default[F[+_, +_]] = Constant[smithy4s.kinds.stubs.Kind2[F]#toKind5]
}

val endpoints: Vector[smithy4s.Endpoint[ServiceWithNullsAndDefaultsOperation, _, _, _, _, _]] = Vector(
ServiceWithNullsAndDefaultsOperation.Operation,
)

def input[I, E, O, SI, SO](op: ServiceWithNullsAndDefaultsOperation[I, E, O, SI, SO]): I = op.input
def ordinal[I, E, O, SI, SO](op: ServiceWithNullsAndDefaultsOperation[I, E, O, SI, SO]): Int = op.ordinal
override def endpoint[I, E, O, SI, SO](op: ServiceWithNullsAndDefaultsOperation[I, E, O, SI, SO]) = op.endpoint
class Constant[P[-_, +_, +_, +_, +_]](value: P[Any, Nothing, Nothing, Nothing, Nothing]) extends ServiceWithNullsAndDefaultsOperation.Transformed[ServiceWithNullsAndDefaultsOperation, P](reified, const5(value))
type Default[F[+_]] = Constant[smithy4s.kinds.stubs.Kind1[F]#toKind5]
def reified: ServiceWithNullsAndDefaultsGen[ServiceWithNullsAndDefaultsOperation] = ServiceWithNullsAndDefaultsOperation.reified
def mapK5[P[_, _, _, _, _], P1[_, _, _, _, _]](alg: ServiceWithNullsAndDefaultsGen[P], f: PolyFunction5[P, P1]): ServiceWithNullsAndDefaultsGen[P1] = new ServiceWithNullsAndDefaultsOperation.Transformed(alg, f)
def fromPolyFunction[P[_, _, _, _, _]](f: PolyFunction5[ServiceWithNullsAndDefaultsOperation, P]): ServiceWithNullsAndDefaultsGen[P] = new ServiceWithNullsAndDefaultsOperation.Transformed(reified, f)
def toPolyFunction[P[_, _, _, _, _]](impl: ServiceWithNullsAndDefaultsGen[P]): PolyFunction5[ServiceWithNullsAndDefaultsOperation, P] = ServiceWithNullsAndDefaultsOperation.toPolyFunction(impl)

}

sealed trait ServiceWithNullsAndDefaultsOperation[Input, Err, Output, StreamedInput, StreamedOutput] {
def run[F[_, _, _, _, _]](impl: ServiceWithNullsAndDefaultsGen[F]): F[Input, Err, Output, StreamedInput, StreamedOutput]
def ordinal: Int
def input: Input
def endpoint: Endpoint[ServiceWithNullsAndDefaultsOperation, Input, Err, Output, StreamedInput, StreamedOutput]
}

object ServiceWithNullsAndDefaultsOperation {

object reified extends ServiceWithNullsAndDefaultsGen[ServiceWithNullsAndDefaultsOperation] {
def operation(input: OperationInput): Operation = Operation(input)
}
class Transformed[P[_, _, _, _, _], P1[_ ,_ ,_ ,_ ,_]](alg: ServiceWithNullsAndDefaultsGen[P], f: PolyFunction5[P, P1]) extends ServiceWithNullsAndDefaultsGen[P1] {
def operation(input: OperationInput): P1[OperationInput, Nothing, OperationOutput, Nothing, Nothing] = f[OperationInput, Nothing, OperationOutput, Nothing, Nothing](alg.operation(input))
}

def toPolyFunction[P[_, _, _, _, _]](impl: ServiceWithNullsAndDefaultsGen[P]): PolyFunction5[ServiceWithNullsAndDefaultsOperation, P] = new PolyFunction5[ServiceWithNullsAndDefaultsOperation, P] {
def apply[I, E, O, SI, SO](op: ServiceWithNullsAndDefaultsOperation[I, E, O, SI, SO]): P[I, E, O, SI, SO] = op.run(impl)
}
final case class Operation(input: OperationInput) extends ServiceWithNullsAndDefaultsOperation[OperationInput, Nothing, OperationOutput, Nothing, Nothing] {
def run[F[_, _, _, _, _]](impl: ServiceWithNullsAndDefaultsGen[F]): F[OperationInput, Nothing, OperationOutput, Nothing, Nothing] = impl.operation(input)
def ordinal: Int = 0
def endpoint: smithy4s.Endpoint[ServiceWithNullsAndDefaultsOperation,OperationInput, Nothing, OperationOutput, Nothing, Nothing] = Operation
}
object Operation extends smithy4s.Endpoint[ServiceWithNullsAndDefaultsOperation,OperationInput, Nothing, OperationOutput, Nothing, Nothing] {
val schema: OperationSchema[OperationInput, Nothing, OperationOutput, Nothing, Nothing] = Schema.operation(ShapeId("smithy4s.example", "Operation"))
.withInput(OperationInput.schema)
.withOutput(OperationOutput.schema)
.withHints(smithy.api.Http(method = smithy.api.NonEmptyString("POST"), uri = smithy.api.NonEmptyString("/operation/{requiredLabel}"), code = 200))
def wrap(input: OperationInput): Operation = Operation(input)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package smithy4s
package object example {
type ErrorHandlingService[F[_]] = smithy4s.kinds.FunctorAlgebra[ErrorHandlingServiceGen, F]
val ErrorHandlingService = ErrorHandlingServiceGen
type ServiceWithNullsAndDefaults[F[_]] = smithy4s.kinds.FunctorAlgebra[ServiceWithNullsAndDefaultsGen, F]
val ServiceWithNullsAndDefaults = ServiceWithNullsAndDefaultsGen
@deprecated(message = "N/A", since = "N/A")
type DeprecatedService[F[_]] = smithy4s.kinds.FunctorAlgebra[DeprecatedServiceGen, F]
val DeprecatedService = DeprecatedServiceGen
Expand Down
Loading

0 comments on commit 2a5007a

Please sign in to comment.