Skip to content

Commit fc061c7

Browse files
Merge pull request #542 from hanny24/string-byte
Handle byte string format according to specification
2 parents ab46593 + c8c7995 commit fc061c7

File tree

9 files changed

+145
-19
lines changed

9 files changed

+145
-19
lines changed

build.sbt

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ assemblyMergeStrategy in assembly := {
3939
oldStrategy(x)
4040
}
4141

42+
val exampleFrameworkSuites = Map(
43+
"scala" -> List(
44+
("akka-http", "akkaHttp", List("client", "server")),
45+
("endpoints", "endpoints", List("client")),
46+
("http4s", "http4s", List("client", "server"))
47+
),
48+
"java" -> List(
49+
("dropwizard", "dropwizard", List("client", "server")),
50+
("spring-mvc", "springMvc", List("server"))
51+
)
52+
)
53+
54+
55+
val scalaFrameworks = exampleFrameworkSuites("scala").map(_._2)
56+
val javaFrameworks = exampleFrameworkSuites("java").map(_._2)
57+
4258
import com.twilio.guardrail.sbt.ExampleCase
4359
def sampleResource(name: String): java.io.File = file(s"modules/sample/src/main/resources/${name}")
4460
val exampleCases: List[ExampleCase] = List(
@@ -77,27 +93,16 @@ val exampleCases: List[ExampleCase] = List(
7793
ExampleCase(sampleResource("plain.json"), "tests.dtos"),
7894
ExampleCase(sampleResource("polymorphism.yaml"), "polymorphism"),
7995
ExampleCase(sampleResource("polymorphism-mapped.yaml"), "polymorphismMapped"),
80-
ExampleCase(sampleResource("polymorphism-nested.yaml"), "polymorphismNested").frameworks(Set("akka-http", "endpoints", "http4s")),
96+
ExampleCase(sampleResource("polymorphism-nested.yaml"), "polymorphismNested").frameworks(scalaFrameworks.toSet),
8197
ExampleCase(sampleResource("raw-response.yaml"), "raw"),
8298
ExampleCase(sampleResource("redaction.yaml"), "redaction"),
8399
ExampleCase(sampleResource("server1.yaml"), "tracer").args("--tracing"),
84100
ExampleCase(sampleResource("server2.yaml"), "tracer").args("--tracing"),
85101
ExampleCase(sampleResource("pathological-parameters.yaml"), "pathological"),
86102
ExampleCase(sampleResource("response-headers.yaml"), "responseHeaders"),
87103
ExampleCase(sampleResource("binary.yaml"), "binary").frameworks(Set("http4s")),
88-
ExampleCase(sampleResource("conflicting-names.yaml"), "conflictingNames")
89-
)
90-
91-
val exampleFrameworkSuites = Map(
92-
"scala" -> List(
93-
("akka-http", "akkaHttp", List("client", "server")),
94-
("endpoints", "endpoints", List("client")),
95-
("http4s", "http4s", List("client", "server"))
96-
),
97-
"java" -> List(
98-
("dropwizard", "dropwizard", List("client", "server")),
99-
("spring-mvc", "springMvc", List("server"))
100-
)
104+
ExampleCase(sampleResource("conflicting-names.yaml"), "conflictingNames"),
105+
ExampleCase(sampleResource("base64.yaml"), "base64").frameworks(scalaFrameworks.toSet),
101106
)
102107

103108
def exampleArgs(language: String): List[List[String]] = exampleCases
@@ -142,9 +147,6 @@ artifact in (Compile, assembly) := {
142147

143148
addArtifact(artifact in (Compile, assembly), assembly)
144149

145-
val scalaFrameworks = exampleFrameworkSuites("scala").map(_._2)
146-
val javaFrameworks = exampleFrameworkSuites("java").map(_._2)
147-
148150
addCommandAlias("resetSample", "; " ++ (scalaFrameworks ++ javaFrameworks).map(x => s"${x}Sample/clean").mkString(" ; "))
149151

150152
// Deprecated command

modules/codegen/src/main/scala/com/twilio/guardrail/SwaggerUtil.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ object SwaggerUtil {
281281
case (Some("string"), Some("email")) => stringType(None)
282282
case (Some("string"), Some("date")) => dateType()
283283
case (Some("string"), Some("date-time")) => dateTimeType()
284+
case (Some("string"), Some("byte")) => bytesType()
284285
case (Some("string"), fmt @ Some("binary")) => fileType(None).map(log(fmt, _))
285286
case (Some("string"), fmt) => stringType(fmt).map(log(fmt, _))
286287
case (Some("number"), Some("float")) => floatType()

modules/codegen/src/main/scala/com/twilio/guardrail/generators/JavaGenerator.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ object JavaGenerator {
240240
)
241241
)
242242

243+
case BytesType() => Target.raiseError("format: bytes not supported for Java")
243244
case DateType() => safeParseType("java.time.LocalDate")
244245
case DateTimeType() => safeParseType("java.time.OffsetDateTime")
245246
case UUIDType() => safeParseType("java.util.UUID")

modules/codegen/src/main/scala/com/twilio/guardrail/generators/ScalaGenerator.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ object ScalaGenerator {
132132
case AlterMethodParameterName(param, name) =>
133133
Target.pure(param.copy(name = name))
134134

135+
case BytesType() => Target.pure(t"Base64String")
135136
case DateType() => Target.pure(t"java.time.LocalDate")
136137
case DateTimeType() => Target.pure(t"java.time.OffsetDateTime")
137138
case UUIDType() => Target.pure(t"java.util.UUID")
@@ -245,6 +246,22 @@ object ScalaGenerator {
245246
ev.addPath(value)
246247
}
247248
}
249+
250+
class Base64String(val data: Array[Byte]) extends AnyVal {
251+
override def toString() = "Base64String(" + data.toString() + ")"
252+
}
253+
object Base64String {
254+
def apply(bytes: Array[Byte]): Base64String = new Base64String(bytes)
255+
def unapply(value: Base64String): Option[Array[Byte]] = Some(value.data)
256+
private[this] val encoder = java.util.Base64.getEncoder
257+
implicit val encode: Encoder[Base64String] =
258+
Encoder[String].contramap[Base64String](v => new String(encoder.encode(v.data)))
259+
260+
private[this] val decoder = java.util.Base64.getDecoder
261+
implicit val decode: Decoder[Base64String] =
262+
Decoder[String].emapTry(v => scala.util.Try(decoder.decode(v))).map(new Base64String(_))
263+
}
264+
248265
}
249266
"""
250267
Target.pure(Some(WriteTree(pkgPath.resolve("Implicits.scala"), sourceToBytes(implicits))))

modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerm.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ case class SelectType[L <: LA](typeNames: NonEmptyList[String])
5050
case class SelectTerm[L <: LA](termNames: NonEmptyList[String]) extends ScalaTerm[L, L#Term]
5151
case class AlterMethodParameterName[L <: LA](param: L#MethodParameter, name: L#TermName) extends ScalaTerm[L, L#MethodParameter]
5252

53+
case class BytesType[L <: LA]() extends ScalaTerm[L, L#Type]
5354
case class UUIDType[L <: LA]() extends ScalaTerm[L, L#Type]
5455
case class DateType[L <: LA]() extends ScalaTerm[L, L#Type]
5556
case class DateTimeType[L <: LA]() extends ScalaTerm[L, L#Type]

modules/codegen/src/main/scala/com/twilio/guardrail/terms/ScalaTerms.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class ScalaTerms[L <: LA, F[_]](implicit I: InjectK[ScalaTerm[L, ?], F]) {
5555
def alterMethodParameterName(param: L#MethodParameter, name: L#TermName): Free[F, L#MethodParameter] =
5656
Free.inject[ScalaTerm[L, ?], F](AlterMethodParameterName(param, name))
5757

58+
def bytesType(): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](BytesType())
5859
def uuidType(): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](UUIDType())
5960
def dateType(): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](DateType())
6061
def dateTimeType(): Free[F, L#Type] = Free.inject[ScalaTerm[L, ?], F](DateTimeType())

modules/codegen/src/test/scala/tests/core/TypesTest.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class TypesTest extends FunSuite with Matchers with SwaggerSpecRunner {
4040
| date_time:
4141
| type: string
4242
| format: date-time
43+
| byte:
44+
| type: string
45+
| format: byte
4346
| long:
4447
| type: integer
4548
| format: int64
@@ -99,6 +102,7 @@ class TypesTest extends FunSuite with Matchers with SwaggerSpecRunner {
99102
string: Option[String] = None,
100103
date: Option[java.time.LocalDate] = None,
101104
date_time: Option[java.time.OffsetDateTime] = None,
105+
byte: Option[Base64String] = None,
102106
long: Option[Long] = None,
103107
int: Option[Int] = None,
104108
float: Option[Float] = None,
@@ -118,9 +122,9 @@ class TypesTest extends FunSuite with Matchers with SwaggerSpecRunner {
118122
object Types {
119123
implicit val encodeTypes: Encoder.AsObject[Types] = {
120124
val readOnlyKeys = Set[String]()
121-
Encoder.AsObject.instance[Types](a => JsonObject.fromIterable(Vector(("array", a.array.asJson), ("map", a.map.asJson), ("obj", a.obj.asJson), ("bool", a.bool.asJson), ("string", a.string.asJson), ("date", a.date.asJson), ("date_time", a.date_time.asJson), ("long", a.long.asJson), ("int", a.int.asJson), ("float", a.float.asJson), ("double", a.double.asJson), ("number", a.number.asJson), ("integer", a.integer.asJson), ("untyped", a.untyped.asJson), ("custom", a.custom.asJson), ("customComplex", a.customComplex.asJson), ("nested", a.nested.asJson), ("nestedArray", a.nestedArray.asJson), ("requiredArray", a.requiredArray.asJson)))).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key)))
125+
Encoder.AsObject.instance[Types](a => JsonObject.fromIterable(Vector(("array", a.array.asJson), ("map", a.map.asJson), ("obj", a.obj.asJson), ("bool", a.bool.asJson), ("string", a.string.asJson), ("date", a.date.asJson), ("date_time", a.date_time.asJson), ("byte", a.byte.asJson), ("long", a.long.asJson), ("int", a.int.asJson), ("float", a.float.asJson), ("double", a.double.asJson), ("number", a.number.asJson), ("integer", a.integer.asJson), ("untyped", a.untyped.asJson), ("custom", a.custom.asJson), ("customComplex", a.customComplex.asJson), ("nested", a.nested.asJson), ("nestedArray", a.nestedArray.asJson), ("requiredArray", a.requiredArray.asJson)))).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key)))
122126
}
123-
implicit val decodeTypes: Decoder[Types] = new Decoder[Types] { final def apply(c: HCursor): Decoder.Result[Types] = for (v0 <- c.downField("array").as[Option[Vector[Boolean]]]; v1 <- c.downField("map").as[Option[Map[String, Boolean]]]; v2 <- c.downField("obj").as[Option[io.circe.Json]]; v3 <- c.downField("bool").as[Option[Boolean]]; v4 <- c.downField("string").as[Option[String]]; v5 <- c.downField("date").as[Option[java.time.LocalDate]]; v6 <- c.downField("date_time").as[Option[java.time.OffsetDateTime]]; v7 <- c.downField("long").as[Option[Long]]; v8 <- c.downField("int").as[Option[Int]]; v9 <- c.downField("float").as[Option[Float]]; v10 <- c.downField("double").as[Option[Double]]; v11 <- c.downField("number").as[Option[BigDecimal]]; v12 <- c.downField("integer").as[Option[BigInt]]; v13 <- c.downField("untyped").as[Option[io.circe.Json]]; v14 <- c.downField("custom").as[Option[Foo]]; v15 <- c.downField("customComplex").as[Option[Foo[Bar]]]; v16 <- c.downField("nested").as[Option[Types.Nested]]; v17 <- c.downField("nestedArray").as[Option[Vector[Types.NestedArray]]]; v18 <- c.downField("requiredArray").as[Vector[String]]) yield Types(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) }
127+
implicit val decodeTypes: Decoder[Types] = new Decoder[Types] { final def apply(c: HCursor): Decoder.Result[Types] = for (v0 <- c.downField("array").as[Option[Vector[Boolean]]]; v1 <- c.downField("map").as[Option[Map[String, Boolean]]]; v2 <- c.downField("obj").as[Option[io.circe.Json]]; v3 <- c.downField("bool").as[Option[Boolean]]; v4 <- c.downField("string").as[Option[String]]; v5 <- c.downField("date").as[Option[java.time.LocalDate]]; v6 <- c.downField("date_time").as[Option[java.time.OffsetDateTime]]; v7 <- c.downField("byte").as[Option[Base64String]]; v8 <- c.downField("long").as[Option[Long]]; v9 <- c.downField("int").as[Option[Int]]; v10 <- c.downField("float").as[Option[Float]]; v11 <- c.downField("double").as[Option[Double]]; v12 <- c.downField("number").as[Option[BigDecimal]]; v13 <- c.downField("integer").as[Option[BigInt]]; v14 <- c.downField("untyped").as[Option[io.circe.Json]]; v15 <- c.downField("custom").as[Option[Foo]]; v16 <- c.downField("customComplex").as[Option[Foo[Bar]]]; v17 <- c.downField("nested").as[Option[Types.Nested]]; v18 <- c.downField("nestedArray").as[Option[Vector[Types.NestedArray]]]; v19 <- c.downField("requiredArray").as[Vector[String]]) yield Types(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) }
124128
case class Nested(prop1: Option[String] = None)
125129
object Nested {
126130
implicit val encodeNested: Encoder.AsObject[Nested] = {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package core.issues
2+
3+
import cats.effect.IO
4+
import cats.data.Kleisli
5+
import org.http4s._
6+
import org.http4s.circe._
7+
import org.http4s.client.{ Client => Http4sClient }
8+
import org.http4s.headers._
9+
import org.http4s.implicits._
10+
import cats.instances.future._
11+
import org.scalatest.concurrent.ScalaFutures
12+
import org.scalatest.time.SpanSugar._
13+
import org.scalatest.{ EitherValues, FunSuite, Matchers, OptionValues }
14+
import tests.scalatest.EitherTValues
15+
16+
class Issue542Suite extends FunSuite with Matchers with EitherValues with ScalaFutures with EitherTValues with OptionValues {
17+
override implicit val patienceConfig = PatienceConfig(10 seconds, 1 second)
18+
19+
test("base64 bytes can be sent") {
20+
import base64.server.http4s.{ FooResponse, Handler, Resource }
21+
import base64.server.http4s.definitions.Foo
22+
import base64.server.http4s.Implicits.Base64String
23+
24+
val route = new Resource[IO]().routes(new Handler[IO] {
25+
def foo(respond: FooResponse.type)(): IO[FooResponse] = IO.pure(respond.Ok(Foo(Some(new Base64String("foo".getBytes())))))
26+
})
27+
28+
val client = Http4sClient.fromHttpApp[IO](route.orNotFound)
29+
30+
val req = Request[IO](method = Method.GET, uri = Uri.unsafeFromString("/foo"))
31+
32+
client
33+
.fetch(req)({
34+
case Status.Ok(resp) =>
35+
resp.status should equal(Status.Ok)
36+
resp.contentType should equal(Some(`Content-Type`(MediaType.application.json)))
37+
resp.contentLength should equal(Some(16))
38+
jsonOf[IO, Foo].decode(resp, strict = false).rightValue
39+
})
40+
.unsafeRunSync()
41+
.value
42+
.value
43+
.data should equal("foo".getBytes())
44+
}
45+
46+
test("base64 bytes can be received") {
47+
import base64.client.http4s.Client
48+
import base64.client.http4s.definitions.Foo
49+
import base64.client.http4s.Implicits.Base64String
50+
import org.http4s.dsl._
51+
52+
def staticClient: Http4sClient[IO] = {
53+
implicit val fooOkEncoder = jsonEncoderOf[IO, Foo]
54+
val response = new Http4sDsl[IO] {
55+
def route: HttpApp[IO] = Kleisli.liftF(Ok(Foo(Some(Base64String("foo".getBytes())))))
56+
}
57+
Http4sClient.fromHttpApp[IO](response.route)
58+
}
59+
60+
Client
61+
.httpClient(staticClient, "http://localhost:80")
62+
.foo()
63+
.attempt
64+
.unsafeRunSync()
65+
.fold(
66+
_ => fail("Error"),
67+
_.fold(
68+
handleOk = _.value.value.data should equal("foo".getBytes())
69+
)
70+
)
71+
}
72+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
swagger: "2.0"
2+
info:
3+
title: Whatever
4+
version: 1.0.0
5+
host: localhost:1234
6+
schemes:
7+
- http
8+
consumes:
9+
- application/json
10+
produces:
11+
- application/json
12+
paths:
13+
/foo:
14+
get:
15+
operationId: foo
16+
responses:
17+
'200':
18+
description: foo
19+
schema:
20+
$ref: '#/definitions/Foo'
21+
definitions:
22+
Foo:
23+
type: object
24+
properties:
25+
value:
26+
type: string
27+
format: byte

0 commit comments

Comments
 (0)