Skip to content

Commit

Permalink
Merge pull request #995 from disneystreaming/explicit-null-017
Browse files Browse the repository at this point in the history
Explicit null for series/0.17
  • Loading branch information
Baccata authored Jun 2, 2023
2 parents 70e1c23 + 51cf347 commit 68a10cc
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 10 deletions.
6 changes: 5 additions & 1 deletion modules/aws/src/smithy4s/json/AwsSchemaVisitorJCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import smithy4s.schema.CompilationCache
import smithy4s.schema.Primitive

private[aws] class AwsSchemaVisitorJCodec(cache: CompilationCache[JCodec])
extends SchemaVisitorJCodec(maxArity = 1024, cache) {
extends SchemaVisitorJCodec(
maxArity = 1024,
explicitNullEncoding = false,
cache
) {

override def primitive[P](
shapeId: ShapeId,
Expand Down
2 changes: 1 addition & 1 deletion modules/decline/src/core/OptsVisitor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ object OptsVisitor extends SchemaVisitor[Opts] { self =>
}

private def parseJson[A](schema: Schema[A]): String => Either[String, A] = {
val capi = smithy4s.http.json.codecs()
val capi = new smithy4s.http.json.JsonCodecs()
val codec = capi.compileCodec(schema)

s =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,15 @@ The `SimpleRestJson` protocol supports 3 different union encodings :
See the section about [unions](../../04-codegen/02-unions.md) for a detailed description.

## Json Array Arity

* By default there is a limit on the arity of an array, which is 1024. This is to prevent the server from being overloaded with a large array as this is a vector for attacks.
* This limit can be changed by setting the maxArity `smithy4s.http4s.SimpleRestJsonBuilder.withMaxArity(.)` to the desired value.
* an example can be seen in the [client example](03-client.md)

## Explicit Null Encoding

By default, optional structure fields that are set to `None` will be excluded from encoded structures. If you wish to change this so that instead they are included and set to `null` explicitly, you can do so by calling `.withExplicitNullEncoding(true)`.

## Supported traits

Here is the list of traits supported by `SimpleRestJson`
Expand Down
24 changes: 18 additions & 6 deletions modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,30 @@ package http4s

import smithy4s.internals.InputOutput

object SimpleRestJsonBuilder extends SimpleRestJsonBuilder(1024)
object SimpleRestJsonBuilder extends SimpleRestJsonBuilder(1024, false)

class SimpleRestJsonBuilder(maxArity: Int)
extends SimpleProtocolBuilder[alloy.SimpleRestJson](
smithy4s.http.json.codecs(
class SimpleRestJsonBuilder private (
maxArity: Int,
explicitNullEncoding: Boolean
) extends SimpleProtocolBuilder[alloy.SimpleRestJson](
new smithy4s.http.json.JsonCodecs(
alloy.SimpleRestJson.protocol.hintMask ++ HintMask(
InputOutput,
IntEnum
),
maxArity
maxArity,
explicitNullEncoding
)
) {

@deprecated("Use builder pattern instead of directly instantiating")
def this(maxArity: Int) = this(maxArity, explicitNullEncoding = false)

def withMaxArity(maxArity: Int): SimpleRestJsonBuilder =
new SimpleRestJsonBuilder(maxArity)
new SimpleRestJsonBuilder(maxArity, explicitNullEncoding)

def withExplicitNullEncoding(
explicitNullEncoding: Boolean
): SimpleRestJsonBuilder =
new SimpleRestJsonBuilder(maxArity, explicitNullEncoding)
}
7 changes: 7 additions & 0 deletions modules/json/src/smithy4s/http/json/SchemaVisitorJCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import scala.collection.mutable.{Map => MMap}

private[smithy4s] class SchemaVisitorJCodec(
maxArity: Int,
explicitNullEncoding: Boolean,
val cache: CompilationCache[JCodec]
) extends SchemaVisitor.Cached[JCodec] { self =>
private val emptyMetadata: MMap[String, Any] = MMap.empty
Expand Down Expand Up @@ -1137,6 +1138,11 @@ private[smithy4s] class SchemaVisitorJCodec(
): (Z, JsonWriter) => Unit = {
val codec = apply(instance)
val jLabel = jsonLabel(field)
val encodeOptionNone: JsonWriter => Unit =
if (explicitNullEncoding) { (out: JsonWriter) =>
out.writeNonEscapedAsciiKey(jLabel)
out.writeNull()
} else (out: JsonWriter) => ()
if (jLabel.forall(JsonWriter.isNonEscapedAscii)) {
(z: Z, out: JsonWriter) =>
{
Expand All @@ -1145,6 +1151,7 @@ private[smithy4s] class SchemaVisitorJCodec(
out.writeNonEscapedAsciiKey(jLabel)
codec.encodeValue(aa, out)
case _ =>
encodeOptionNone(out)
}
}
} else { (z: Z, out: JsonWriter) =>
Expand Down
17 changes: 16 additions & 1 deletion modules/json/src/smithy4s/http/json/codecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ import smithy4s.internals.InputOutput
import smithy4s.schema.CompilationCache
import smithy4s.schema.SchemaVisitor

final class JsonCodecs(
hintMask: HintMask = codecs.defaultHintMask,
maxArity: Int = codecs.defaultMaxArity,
explicitNullEncoding: Boolean = false
) extends JsonCodecAPI(
cache =>
new SchemaVisitorJCodec(
maxArity = maxArity,
explicitNullEncoding = explicitNullEncoding,
cache = cache
),
Some(hintMask)
)

final case class codecs(
hintMask: HintMask = codecs.defaultHintMask,
maxArity: Int = codecs.defaultMaxArity
Expand All @@ -51,11 +65,12 @@ object codecs {
cache: CompilationCache[JCodec],
maxArity: Int = defaultMaxArity
): SchemaVisitor[JCodec] =
new SchemaVisitorJCodec(maxArity, cache)
new SchemaVisitorJCodec(maxArity, false, cache)

private[smithy4s] val schemaVisitorJCodec: SchemaVisitor[JCodec] =
new SchemaVisitorJCodec(
maxArity = defaultMaxArity,
explicitNullEncoding = false,
CompilationCache.nop[JCodec]
)

Expand Down
41 changes: 40 additions & 1 deletion modules/json/test/src/smithy4s/http/json/JsonCodecApiTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,51 @@ class JsonCodecApiTests extends FunSuite {
.required[String]("a", identity)
)(identity)

val capi = codecs(HintMask.empty)
val capi = new JsonCodecs(HintMask.empty)

val codec = capi.compileCodec(schemaWithRequiredField)

val decoded = capi.decodeFromByteArray(codec, """{}""".getBytes())

assert(decoded.isLeft)
}

test(
"explicit nulls should be used when set"
) {
val schemaWithJsonName = Schema
.struct[Option[String]]
.apply(
Schema.string
.optional[Option[String]]("a", identity)
)(identity)

val capi = new JsonCodecs(HintMask.empty, explicitNullEncoding = true)

val codec = capi.compileCodec(schemaWithJsonName)
val encodedString = new String(capi.writeToArray(codec, None))

assertEquals(encodedString, """{"a":null}""")
}

test(
"explicit nulls should be parsable regardless of explicitNullEncoding setting"
) {
List(true, false).foreach { nullEncoding =>
val schemaWithJsonName = Schema
.struct[Option[String]]
.apply(
Schema.string
.optional[Option[String]]("a", identity)
)(identity)

val capi =
new JsonCodecs(HintMask.empty, explicitNullEncoding = nullEncoding)

val codec = capi.compileCodec(schemaWithJsonName)
val decoded = capi.decodeFromByteArray(codec, """{"a":null}""".getBytes())

assertEquals(decoded, Right(None))
}
}
}

0 comments on commit 68a10cc

Please sign in to comment.