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

Support for sparse collections in query params #1513

Open
wants to merge 13 commits into
base: series/0.19
Choose a base branch
from
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Thank you!

# 0.19.0

## Update HttpUri model to allow optional query parameters in [1513](https://github.com/disneystreaming/smithy4s/pull/1513)

Change the type of query parameters in `HttpUri` from `queryParams: Map[String, Seq[String]]` to `queryParams: Map[String, Seq[Option[String]]]` to allow
modeling of sparse collections.

## Documentation fix

Prevent documentation from being generated for case class when the field are not generated because they're annotated with `@streaming`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"openapi": "3.0.2",
"info": {
"title": "ServiceWithSparseQueryParams",
"version": "1.0"
},
"paths": {
"/operation/sparse-query-params": {
"get": {
"operationId": "GetOperation",
"parameters": [
{
"name": "foo",
"in": "query",
"style": "form",
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"explode": true,
"required": true
}
],
"responses": {
"200": {
"description": "GetOperation 200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetOperationResponseContent"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"GetOperationResponseContent": {
"type": "object",
"properties": {
"foo": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"foo"
]
}
}
}
}
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 ServiceWithSparseQueryParamsGen[F[_, _, _, _, _]] {
self =>

def getOperation(foo: List[Option[String]]): F[SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing]

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

object ServiceWithSparseQueryParamsGen extends Service.Mixin[ServiceWithSparseQueryParamsGen, ServiceWithSparseQueryParamsOperation] {

val id: ShapeId = ShapeId("smithy4s.example", "ServiceWithSparseQueryParams")
val version: String = "1.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[ServiceWithSparseQueryParamsOperation, _, _, _, _, _]] = Vector(
ServiceWithSparseQueryParamsOperation.GetOperation,
)

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

}

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

object ServiceWithSparseQueryParamsOperation {

object reified extends ServiceWithSparseQueryParamsGen[ServiceWithSparseQueryParamsOperation] {
def getOperation(foo: List[Option[String]]): GetOperation = GetOperation(SparseQueryInput(foo))
}
class Transformed[P[_, _, _, _, _], P1[_ ,_ ,_ ,_ ,_]](alg: ServiceWithSparseQueryParamsGen[P], f: PolyFunction5[P, P1]) extends ServiceWithSparseQueryParamsGen[P1] {
def getOperation(foo: List[Option[String]]): P1[SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing] = f[SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing](alg.getOperation(foo))
}

def toPolyFunction[P[_, _, _, _, _]](impl: ServiceWithSparseQueryParamsGen[P]): PolyFunction5[ServiceWithSparseQueryParamsOperation, P] = new PolyFunction5[ServiceWithSparseQueryParamsOperation, P] {
def apply[I, E, O, SI, SO](op: ServiceWithSparseQueryParamsOperation[I, E, O, SI, SO]): P[I, E, O, SI, SO] = op.run(impl)
}
final case class GetOperation(input: SparseQueryInput) extends ServiceWithSparseQueryParamsOperation[SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing] {
def run[F[_, _, _, _, _]](impl: ServiceWithSparseQueryParamsGen[F]): F[SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing] = impl.getOperation(input.foo)
def ordinal: Int = 0
def endpoint: smithy4s.Endpoint[ServiceWithSparseQueryParamsOperation,SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing] = GetOperation
}
object GetOperation extends smithy4s.Endpoint[ServiceWithSparseQueryParamsOperation,SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing] {
val schema: OperationSchema[SparseQueryInput, Nothing, SparseQueryOutput, Nothing, Nothing] = Schema.operation(ShapeId("smithy4s.example", "GetOperation"))
.withInput(SparseQueryInput.schema)
.withOutput(SparseQueryOutput.schema)
.withHints(smithy.api.Http(method = smithy.api.NonEmptyString("GET"), uri = smithy.api.NonEmptyString("/operation/sparse-query-params"), code = 200))
def wrap(input: SparseQueryInput): GetOperation = GetOperation(input)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package smithy4s.example

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

final case class SparseQueryInput(foo: List[Option[String]])

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

val hints: Hints = Hints.empty

// constructor using the original order from the spec
private def make(foo: List[Option[String]]): SparseQueryInput = SparseQueryInput(foo)

implicit val schema: Schema[SparseQueryInput] = struct(
SparseStringList.underlyingSchema.required[SparseQueryInput]("foo", _.foo).addHints(smithy.api.HttpQuery("foo")),
)(make).withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package smithy4s.example

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

final case class SparseQueryOutput(foo: List[Option[String]])

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

val hints: Hints = Hints.empty

// constructor using the original order from the spec
private def make(foo: List[Option[String]]): SparseQueryOutput = SparseQueryOutput(foo)

implicit val schema: Schema[SparseQueryOutput] = struct(
SparseStringList.underlyingSchema.required[SparseQueryOutput]("foo", _.foo),
)(make).withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package object example {
val PizzaAdminService = PizzaAdminServiceGen
type FooService[F[_]] = smithy4s.kinds.FunctorAlgebra[FooServiceGen, F]
val FooService = FooServiceGen
type ServiceWithSparseQueryParams[F[_]] = smithy4s.kinds.FunctorAlgebra[ServiceWithSparseQueryParamsGen, F]
val ServiceWithSparseQueryParams = ServiceWithSparseQueryParamsGen
type KVStore[F[_]] = smithy4s.kinds.FunctorAlgebra[KVStoreGen, F]
val KVStore = KVStoreGen
type ObjectService[F[_]] = smithy4s.kinds.FunctorAlgebra[ObjectServiceGen, F]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class MetadataSpec() extends FunSuite {
val queries = Queries(str = Some("hello"))
val finished =
Queries(str = Some("hello"), slm = Some(Map("str" -> "hello")))
val expected = Metadata(query = Map("str" -> List("hello")))
val expected = Metadata(query = Map("str" -> List(Some("hello"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -131,7 +131,7 @@ class MetadataSpec() extends FunSuite {
test("String length constraint violation") {
val string = "1" * 11
val queries = ValidationChecks(str = Some(string))
val expected = Metadata(query = Map("str" -> List(string)))
val expected = Metadata(query = Map("str" -> List(Some(string))))
checkRoundTripError(
queries,
expected,
Expand All @@ -142,7 +142,7 @@ class MetadataSpec() extends FunSuite {
test("List length constraint violation") {
val list = List.fill(11)("str")
val queries = ValidationChecks(lst = Some(list))
val expected = Metadata(query = Map("lst" -> list))
val expected = Metadata(query = Map("lst" -> list.map(Some(_))))
checkRoundTripError(
queries,
expected,
Expand All @@ -153,7 +153,7 @@ class MetadataSpec() extends FunSuite {
test("Integer range constraint violation") {
val i = 11
val queries = ValidationChecks(int = Some(i))
val expected = Metadata(query = Map("int" -> List(i.toString)))
val expected = Metadata(query = Map("int" -> List(Some(i.toString))))
checkRoundTripError(
queries,
expected,
Expand All @@ -164,14 +164,14 @@ class MetadataSpec() extends FunSuite {
test("Integer query parameter") {
val queries = Queries(int = Some(123))
val finished = Queries(int = Some(123), slm = Some(Map("int" -> "123")))
val expected = Metadata(query = Map("int" -> List("123")))
val expected = Metadata(query = Map("int" -> List(Some("123"))))
checkQueryRoundTrip(queries, expected, finished)
}

test("Boolean query parameter") {
val queries = Queries(b = Some(true))
val finished = Queries(b = Some(true), slm = Some(Map("b" -> "true")))
val expected = Metadata(query = Map("b" -> List("true")))
val expected = Metadata(query = Map("b" -> List(Some("true"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -180,7 +180,7 @@ class MetadataSpec() extends FunSuite {
val queries = Queries(ts1 = Some(ts))
val finished =
Queries(ts1 = Some(ts), slm = Some(Map("ts1" -> epochString)))
val expected = Metadata(query = Map("ts1" -> List(epochString)))
val expected = Metadata(query = Map("ts1" -> List(Some(epochString))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -189,7 +189,7 @@ class MetadataSpec() extends FunSuite {
val queries = Queries(ts2 = Some(ts))
val finished =
Queries(ts2 = Some(ts), slm = Some(Map("ts2" -> epochString)))
val expected = Metadata(query = Map("ts2" -> List(epochString)))
val expected = Metadata(query = Map("ts2" -> List(Some(epochString))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -198,7 +198,7 @@ class MetadataSpec() extends FunSuite {
val queries = Queries(ts3 = Some(ts))
val finished =
Queries(ts3 = Some(ts), slm = Some(Map("ts3" -> "1234567890")))
val expected = Metadata(query = Map("ts3" -> List("1234567890")))
val expected = Metadata(query = Map("ts3" -> List(Some("1234567890"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -210,23 +210,25 @@ class MetadataSpec() extends FunSuite {
slm = Some(Map("ts4" -> "Thu, 01 Jan 1970 00:00:00 GMT"))
)
val expected =
Metadata(query = Map("ts4" -> List("Thu, 01 Jan 1970 00:00:00 GMT")))
Metadata(query =
Map("ts4" -> List(Some("Thu, 01 Jan 1970 00:00:00 GMT")))
)
checkQueryRoundTrip(queries, expected, finished)
}

test("map of strings query param") {
val map =
Map("hello" -> "a", "world" -> "b")
val queries = Queries(slm = Some(map))
val expected = Metadata(query = map.fmap(Seq(_)))
val expected = Metadata(query = map.fmap(a => Seq(Some(a))))
checkRoundTrip(queries, expected)
}

test("list of strings query param") {
val list = List("hello", "world")
val queries = Queries(sl = Some(list))
val finished = Queries(sl = Some(list), slm = Some(Map("sl" -> "hello")))
val expected = Metadata(query = Map("sl" -> list))
val expected = Metadata(query = Map("sl" -> list.map(Some(_))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -237,7 +239,7 @@ class MetadataSpec() extends FunSuite {
ie = Some(smithy4s.example.Numbers.ONE),
slm = Some(Map("nums" -> "1"))
)
val expected = Metadata(query = Map("nums" -> List("1")))
val expected = Metadata(query = Map("nums" -> List(Some("1"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -248,7 +250,7 @@ class MetadataSpec() extends FunSuite {
on = Some(smithy4s.example.OpenNums.$Unknown(101)),
slm = Some(Map("openNums" -> "101"))
)
val expected = Metadata(query = Map("openNums" -> List("101")))
val expected = Metadata(query = Map("openNums" -> List(Some("101"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -259,7 +261,7 @@ class MetadataSpec() extends FunSuite {
on = Some(smithy4s.example.OpenNums.ONE),
slm = Some(Map("openNums" -> "1"))
)
val expected = Metadata(query = Map("openNums" -> List("1")))
val expected = Metadata(query = Map("openNums" -> List(Some("1"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -271,7 +273,7 @@ class MetadataSpec() extends FunSuite {
ons = Some(smithy4s.example.OpenNumsStr.$Unknown("test")),
slm = Some(Map("openNumsStr" -> "test"))
)
val expected = Metadata(query = Map("openNumsStr" -> List("test")))
val expected = Metadata(query = Map("openNumsStr" -> List(Some("test"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand All @@ -282,7 +284,7 @@ class MetadataSpec() extends FunSuite {
ons = Some(smithy4s.example.OpenNumsStr.ONE),
slm = Some(Map("openNumsStr" -> "ONE"))
)
val expected = Metadata(query = Map("openNumsStr" -> List("ONE")))
val expected = Metadata(query = Map("openNumsStr" -> List(Some("ONE"))))
checkQueryRoundTrip(queries, expected, finished)
}

Expand Down Expand Up @@ -439,7 +441,9 @@ class MetadataSpec() extends FunSuite {

test("bad data gets caught") {
val metadata =
Metadata(query = Map("ts3" -> List("Thu, 01 Jan 1970 00:00:00 GMT")))
Metadata(query =
Map("ts3" -> List(Some("Thu, 01 Jan 1970 00:00:00 GMT")))
)
val result = Metadata.decode[Queries](metadata)
val expected = MetadataError.WrongType(
"ts3",
Expand All @@ -462,7 +466,7 @@ class MetadataSpec() extends FunSuite {

test("too many parameters get caught") {
val metadata =
Metadata(query = Map("ts3" -> List("1", "2", "3")))
Metadata(query = Map("ts3" -> List("1", "2", "3").map(Some(_))))
val result = Metadata.decode[Queries](metadata)
val expected = MetadataError.ArityError(
"ts3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ class PathSpec() extends munit.FunSuite {
val sqp = httpEndpoint.staticQueryParams
val path = httpEndpoint.path

val expectedQueryMap = Map("value" -> Seq("foo"), "baz" -> Seq("bar"))
val expectedQueryMap =
Map("value" -> Seq(Some("foo")), "baz" -> Seq(Some("bar")))
expect(sqp == expectedQueryMap)
expect(
path ==
Expand Down
Loading