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

Schema based header codecs, unified with query codecs (#3232) #3270

Merged
merged 2 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ jobs:
with:
apps: sbt

- uses: coursier/setup-action@v1
with:
apps: sbt

- name: Release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
Expand Down
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.8.1
version = 3.8.6
maxColumn = 120

align.preset = more
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ThisBuild / githubWorkflowPublishTargetBranches += RefPredicate.StartsWith(Ref.T
ThisBuild / githubWorkflowPublishPreamble := Seq(coursierSetup)
ThisBuild / githubWorkflowPublish :=
Seq(
WorkflowStep.Use(UseRef.Public("coursier", "setup-action", "v1"), Map("apps" -> "sbt")),
WorkflowStep.Sbt(
List("ci-release"),
name = Some("Release"),
Expand Down
4 changes: 2 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ object Dependencies {
val `jwt-core` = "com.github.jwt-scala" %% "jwt-core" % JwtCoreVersion
val `scala-compact-collection` = "org.scala-lang.modules" %% "scala-collection-compat" % ScalaCompactCollectionVersion

val scalafmt = "org.scalameta" %% "scalafmt-dynamic" % "3.8.1"
val scalametaParsers = "org.scalameta" %% "parsers" % "4.9.9"
val scalafmt = "org.scalameta" %% "scalafmt-dynamic" % "3.8.6"
val scalametaParsers = "org.scalameta" %% "parsers" % "4.12.7"

val netty =
Seq(
Expand Down
11 changes: 11 additions & 0 deletions project/MimaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ object MimaSettings {
mimaBinaryIssueFilters ++= Seq(
exclude[Problem]("zio.http.internal.*"),
exclude[Problem]("zio.http.codec.internal.*"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$Record$"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$Record"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$Primitive$"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$Primitive"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$Collection$"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$Collection"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType$"),
exclude[Problem]("zio.http.codec.HttpCodec$Query$QueryType"),
exclude[Problem]("zio.http.endpoint.openapi.OpenAPIGen#AtomizedMetaCodecs.apply"),
exclude[Problem]("zio.http.endpoint.openapi.OpenAPIGen#AtomizedMetaCodecs.this"),
exclude[Problem]("zio.http.endpoint.openapi.OpenAPIGen#AtomizedMetaCodecs.copy"),
),
mimaFailOnProblem := failOnProblem
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ServerInboundHandlerBenchmark {
private val largeString = random.alphanumeric.take(100000).mkString

private val baseUrl = "http://localhost:8080"
private val headers = Headers(Header.ContentType(MediaType.text.`plain`).untyped)
private val headers = Headers(Header.ContentType(MediaType.text.`plain`))

private val arrayEndpoint = "array"
private val arrayResponse = ZIO.succeed(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package zio.http.endpoint.cli

import zio.http._
import zio.http.codec.HttpCodec.Query.QueryType
import zio.http.codec._
import zio.http.endpoint._

Expand Down Expand Up @@ -112,13 +111,9 @@ private[cli] object CliEndpoint {
}
CliEndpoint(body = HttpOptions.Body(name, codec.defaultMediaType, codec.defaultSchema) :: List())

case HttpCodec.Header(name, textCodec, _) if textCodec.isInstanceOf[TextCodec.Constant] =>
CliEndpoint(headers =
HttpOptions.HeaderConstant(name, textCodec.asInstanceOf[TextCodec.Constant].string) :: List(),
)
case HttpCodec.Header(name, textCodec, _) =>
CliEndpoint(headers = HttpOptions.Header(name, textCodec) :: List())
case HttpCodec.Method(codec, _) =>
case HttpCodec.Header(headerType, _) =>
CliEndpoint(headers = HttpOptions.Header(headerType.names.head, TextCodec.string) :: List())
case HttpCodec.Method(codec, _) =>
codec.asInstanceOf[SimpleCodec[_, _]] match {
case SimpleCodec.Specified(method: Method) =>
CliEndpoint(methods = method)
Expand All @@ -128,22 +123,11 @@ private[cli] object CliEndpoint {
case HttpCodec.Path(pathCodec, _) =>
CliEndpoint(url = HttpOptions.Path(pathCodec) :: List())

case HttpCodec.Query(queryType, _) =>
queryType match {
case QueryType.Primitive(name, codec) =>
CliEndpoint(url = HttpOptions.Query(name, codec) :: List())
case record @ QueryType.Record(_) =>
val queryOptions = record.fieldAndCodecs.map { case (field, codec) =>
HttpOptions.Query(field.name, codec)
}
CliEndpoint(url = queryOptions.toList)
case QueryType.Collection(_, elements, _) =>
val queryOptions =
HttpOptions.Query(elements.name, elements.codec)
CliEndpoint(url = queryOptions :: List())
}

case HttpCodec.Status(_, _) => CliEndpoint.empty
case HttpCodec.Query(codec, _) =>
CliEndpoint(url = codec.recordFields.map { case (f, codec) =>
HttpOptions.Query(codec, f.fieldName)
}.toList)
case HttpCodec.Status(_, _) => CliEndpoint.empty

}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import zio.schema.annotation.description

import zio.http._
import zio.http.codec._
import zio.http.internal.StringSchemaCodec
import zio.http.internal.StringSchemaCodec.PrimitiveCodec

/*
* HttpOptions is a wrapper of a transformation Options[CliRequest] => Options[CliRequest].
Expand Down Expand Up @@ -264,12 +266,10 @@ private[cli] object HttpOptions {

}

final case class Query(override val name: String, codec: BinaryCodecWithSchema[_], doc: Doc = Doc.empty)
extends URLOptions {
final case class Query(codec: PrimitiveCodec[_], name: String, doc: Doc = Doc.empty) extends URLOptions {
self =>

override val tag = "?" + name
def options: Options[_] = optionsFromSchema(codec)(name)
def options: Options[_] = optionsFromSchema(codec.schema)(name)

override def ??(doc: Doc): Query = self.copy(doc = self.doc + doc)

Expand All @@ -293,8 +293,8 @@ private[cli] object HttpOptions {

}

private[cli] def optionsFromSchema[A](codec: BinaryCodecWithSchema[A]): String => Options[A] =
codec.schema match {
private[cli] def optionsFromSchema[A](schema: Schema[A]): String => Options[A] =
schema match {
case Schema.Primitive(standardType, _) =>
standardType match {
case StandardType.UnitType =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ import zio.http.codec._
*/

object AuxGen {
lazy val anyTextCodec: Gen[Any, TextCodec[_]] =
Gen.oneOf(
Gen.fromIterable(List(TextCodec.boolean, TextCodec.int, TextCodec.string, TextCodec.uuid)),
Gen.alphaNumericStringBounded(1, 30).map(TextCodec.constant(_)),
)
lazy val anyTextCodec: Gen[Any, TextCodec[_]] = Gen.const(TextCodec.string)

lazy val anyMediaType: Gen[Any, MediaType] = Gen.fromIterable(MediaType.allMediaTypes)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object CliSpec extends ZIOSpecDefault {

val bodyStream = ContentCodec.contentStream[BigInt]("bodyStream")

val headerCodec = HttpCodec.Header("header", TextCodec.string)
val headerCodec = HttpCodec.headerAs[String]("header")

val path1 = PathCodec.bool("path1")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ object CommandGen {
case _ => true
}.map {
case HttpOptions.Path(pathCodec, _) =>
pathCodec.segments.toList.flatMap { case segment =>
pathCodec.segments.toList.flatMap { segment =>
getSegment(segment) match {
case (_, "") => Nil
case (name, "boolean") => s"[${getName(name, "")}]" :: Nil
case (name, codec) => s"${getName(name, "")} $codec" :: Nil
}
}
case HttpOptions.Query(name, codec, _) =>
getType(codec) match {
case "" => s"[${getName(name, "")}]" :: Nil
case codec => s"${getName(name, "")} $codec" :: Nil
case HttpOptions.Query(codec, name, _) =>
getType(codec.schema) match {
case "" => s"[${getName(name, "")}]" :: Nil
case tpy => s"${getName(name, "")} $tpy" :: Nil
}
case _ => Nil
}.foldRight(List[String]())(_ ++ _)
Expand Down Expand Up @@ -121,8 +121,8 @@ object CommandGen {
case _ => ""
}

def getType[A](codec: BinaryCodecWithSchema[A]): String =
codec.schema match {
def getType[A](schema: Schema[A]): String =
schema match {
case Schema.Primitive(standardType, _) =>
standardType match {
case StandardType.UnitType => ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ object EndpointGen {
lazy val anyHeader: Gen[Any, CliReprOf[Codec[_]]] =
Gen.alphaNumericStringBounded(1, 30).zip(anyTextCodec).map { case (name, codec) =>
CliRepr(
HttpCodec.Header(name, codec),
HttpCodec.Header(Header.Custom(name, "").headerType), // todo use schema bases header
codec match {
case TextCodec.Constant(value) => CliEndpoint(headers = HttpOptions.HeaderConstant(name, value) :: Nil)
case _ => CliEndpoint(headers = HttpOptions.Header(name, codec) :: Nil)
case _ => CliEndpoint(headers = HttpOptions.Header(name, codec) :: Nil)
},
)
}
Expand All @@ -102,10 +101,10 @@ object EndpointGen {
lazy val anyQuery: Gen[Any, CliReprOf[Codec[_]]] =
Gen.alphaNumericStringBounded(1, 30).zip(anyStandardType).map { case (name, schema0) =>
val schema = schema0.asInstanceOf[Schema[Any]]
val codec = BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
val codec = QueryCodec.query(name)(schema).asInstanceOf[HttpCodec.Query[Any]]
CliRepr(
HttpCodec.Query(HttpCodec.Query.QueryType.Primitive(name, codec)),
CliEndpoint(url = HttpOptions.Query(name, codec) :: Nil),
codec,
CliEndpoint(url = HttpOptions.Query(codec.codec.recordFields.head._2, name) :: Nil),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import zio.http._
import zio.http.codec._
import zio.http.endpoint.cli.AuxGen._
import zio.http.endpoint.cli.CliRepr._
import zio.http.internal.StringSchemaCodec.PrimitiveCodec

/**
* Constructs a Gen[Options[CliRequest], CliEndpoint]
Expand All @@ -32,10 +33,10 @@ object OptionsGen {
.optionsFromTextCodec(textCodec)(name)
.map(value => textCodec.encode(value))

def encodeOptions[A](name: String, codec: BinaryCodecWithSchema[A]): Options[String] =
def encodeOptions[A](name: String, codec: PrimitiveCodec[A], schema: Schema[A]): Options[String] =
HttpOptions
.optionsFromSchema(codec)(name)
.map(value => codec.codec(CodecConfig.defaultConfig).encode(value).asString)
.optionsFromSchema(schema)(name)
.map(value => codec.encode(value))

lazy val anyBodyOption: Gen[Any, CliReprOf[Options[Retriever]]] =
Gen
Expand All @@ -50,18 +51,12 @@ object OptionsGen {
}

lazy val anyHeaderOption: Gen[Any, CliReprOf[Options[Headers]]] =
Gen.alphaNumericStringBounded(1, 30).zip(anyTextCodec).map {
case (name, TextCodec.Constant(value)) =>
CliRepr(
Options.Empty.map(_ => Headers(name, value)),
CliEndpoint(headers = HttpOptions.HeaderConstant(name, value) :: Nil),
)
case (name, codec) =>
CliRepr(
encodeOptions(name, codec)
.map(value => Headers(name, value)),
CliEndpoint(headers = HttpOptions.Header(name, codec) :: Nil),
)
Gen.alphaNumericStringBounded(1, 30).zip(anyTextCodec).map { case (name, codec) =>
CliRepr(
encodeOptions(name, codec)
.map(value => Headers(name, value)),
CliEndpoint(headers = HttpOptions.Header(name, codec) :: Nil),
)
}

lazy val anyURLOption: Gen[Any, CliReprOf[Options[String]]] =
Expand All @@ -83,14 +78,12 @@ object OptionsGen {
},
Gen
.alphaNumericStringBounded(1, 30)
.zip(anyStandardType.map { s =>
val schema = s.asInstanceOf[Schema[Any]]
BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
})
.map { case (name, codec) =>
.zip(anyStandardType)
.map { case (name, schema) =>
val codec = QueryCodec.query(name)(schema).asInstanceOf[HttpCodec.Query[Any]]
CliRepr(
encodeOptions(name, codec),
CliEndpoint(url = HttpOptions.Query(name, codec) :: Nil),
encodeOptions(name, codec.codec.recordFields.head._2, schema.asInstanceOf[Schema[Any]]),
CliEndpoint(url = HttpOptions.Query(codec.codec.recordFields.head._2, name) :: Nil),
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ object WebSocketReconnectingClient extends ZIOAppDefault {
channel.send(ChannelEvent.Read(WebSocketFrame.text("foo")))

// On receiving "foo", we'll reply with another "foo" to keep echo loop going
case Read(WebSocketFrame.Text("foo")) =>
case Read(WebSocketFrame.Text("foo")) =>
ZIO.logInfo("Received foo message.") *>
ZIO.sleep(1.second) *>
channel.send(ChannelEvent.Read(WebSocketFrame.text("foo")))

// Handle exception and convert it to failure to signal the shutdown of the socket connection via the promise
case ExceptionCaught(t) =>
case ExceptionCaught(t) =>
ZIO.fail(t)

case _ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ object WebSocketServerAdvanced extends ZIOAppDefault {
val socketApp: WebSocketApp[Any] =
Handler.webSocket { channel =>
channel.receiveAll {
case Read(WebSocketFrame.Text("end")) =>
case Read(WebSocketFrame.Text("end")) =>
channel.shutdown

// Send a "bar" if the client sends a "foo"
case Read(WebSocketFrame.Text("foo")) =>
case Read(WebSocketFrame.Text("foo")) =>
channel.send(Read(WebSocketFrame.text("bar")))

// Send a "foo" if the client sends a "bar"
case Read(WebSocketFrame.Text("bar")) =>
case Read(WebSocketFrame.Text("bar")) =>
channel.send(Read(WebSocketFrame.text("foo")))

// Echo the same message 10 times if it's not "foo" or "bar"
case Read(WebSocketFrame.Text(text)) =>
case Read(WebSocketFrame.Text(text)) =>
channel
.send(Read(WebSocketFrame.text(s"echo $text")))
.repeatN(10)
Expand All @@ -38,11 +38,11 @@ object WebSocketServerAdvanced extends ZIOAppDefault {
channel.send(Read(WebSocketFrame.text("Greetings!")))

// Log when the channel is getting closed
case Read(WebSocketFrame.Close(status, reason)) =>
case Read(WebSocketFrame.Close(status, reason)) =>
Console.printLine("Closing channel with status: " + status + " and reason: " + reason)

// Print the exception if it's not a normal close
case ExceptionCaught(cause) =>
case ExceptionCaught(cause) =>
Console.printLine(s"Channel error!: ${cause.getMessage}")

case _ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ object WebSocketSimpleClient extends ZIOAppDefault {
channel.send(Read(WebSocketFrame.text("foo")))

// Send a "bar" if the server sends a "foo"
case Read(WebSocketFrame.Text("foo")) =>
case Read(WebSocketFrame.Text("foo")) =>
channel.send(Read(WebSocketFrame.text("bar")))

// Close the connection if the server sends a "bar"
case Read(WebSocketFrame.Text("bar")) =>
case Read(WebSocketFrame.Text("bar")) =>
ZIO.succeed(println("Goodbye!")) *> channel.send(Read(WebSocketFrame.close(1000)))

case _ =>
Expand Down
Loading
Loading