Skip to content

Commit

Permalink
Merge branch 'main' into update/munit-cats-effect-2.0.0-M4
Browse files Browse the repository at this point in the history
  • Loading branch information
ybasket authored Jan 28, 2024
2 parents 981d656 + 728922b commit 8943f93
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 214 deletions.
343 changes: 145 additions & 198 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.7.12
version = 3.7.17

style = default

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Home of http4s integrations with [fs2-data][fs2-data]. Initially forked from [ht
* XML and Scala XML. Works as a drop-in replacement for [http4s-scala-xml][http4s-scala-xml]
* CSV
* CBOR
* JSON

Check out the [docs][docs] for examples.

Expand Down
42 changes: 32 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
ThisBuild / tlBaseVersion := "0.2"
ThisBuild / tlBaseVersion := "0.3"
ThisBuild / tlJdkRelease := Some(11)
// exclude Java 8 from CI as fs2-data doesn't support it
ThisBuild / githubWorkflowJavaVersions -= JavaSpec.temurin("8")
ThisBuild / developers := List(
tlGitHubDev("rossabaker", "Ross A. Baker"),
tlGitHubDev("ybasket", "Yannick Heiber"),
)

val Scala213 = "2.13.11"
ThisBuild / crossScalaVersions := Seq("2.12.18", Scala213, "3.3.0")
val Scala213 = "2.13.12"
ThisBuild / crossScalaVersions := Seq("2.12.18", Scala213, "3.3.1")
ThisBuild / scalaVersion := Scala213

// ensure missing timezones don't break tests on JS
Expand All @@ -14,13 +17,17 @@ ThisBuild / jsEnv := {
new NodeJSEnv(NodeJSEnv.Config().withEnv(Map("TZ" -> "UTC")))
}

// ensure we don't fail compilation – package objects with inheritance are used in http4s/http4s as well,
// better to stay style-consistent for now
ThisBuild / scalacOptions += "-Wconf:msg=package object inheritance is deprecated:s"

lazy val root = tlCrossRootProject.aggregate(xml, xmlScala, csv, cbor)

val http4sVersion = "0.23.23"
val http4sVersion = "0.23.25"
val scalaXmlVersion = "2.2.0"
val fs2Version = "3.8.0"
val fs2DataVersion = "1.8.0"
val munitVersion = "1.0.0-M8"
val fs2Version = "3.9.4"
val fs2DataVersion = "1.10.0"
val munitVersion = "1.0.0-M10"
val munitCatsEffectVersion = "2.0.0-M4"

lazy val xml = crossProject(JVMPlatform, JSPlatform, NativePlatform)
Expand Down Expand Up @@ -66,7 +73,6 @@ lazy val csv = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "http4s-fs2-data-csv",
description := "Provides csv codecs for http4s via fs2-data",
startYear := Some(2023),
tlVersionIntroduced := Map("2.12" -> "0.2", "2.13" -> "0.2", "3" -> "0.2"),
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-core" % fs2Version,
"org.http4s" %%% "http4s-core" % http4sVersion,
Expand All @@ -85,7 +91,6 @@ lazy val cbor = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "http4s-fs2-data-cbor",
description := "Provides CBOR codecs for http4s via fs2-data",
startYear := Some(2023),
tlVersionIntroduced := Map("2.12" -> "0.2", "2.13" -> "0.2", "3" -> "0.2"),
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-core" % fs2Version,
"org.http4s" %%% "http4s-core" % http4sVersion,
Expand All @@ -96,9 +101,26 @@ lazy val cbor = crossProject(JVMPlatform, JSPlatform, NativePlatform)
),
)

lazy val json = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("json"))
.settings(
name := "http4s-fs2-data-json",
description := "Provides JSON codecs for http4s via fs2-data",
startYear := Some(2024),
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-core" % fs2Version,
"org.http4s" %%% "http4s-core" % http4sVersion,
"org.gnieh" %%% "fs2-data-json" % fs2DataVersion,
"org.scalameta" %%% "munit-scalacheck" % munitVersion % Test,
"org.typelevel" %%% "munit-cats-effect" % munitCatsEffectVersion % Test,
"org.http4s" %%% "http4s-laws" % http4sVersion % Test,
),
)

lazy val docs = project
.in(file("site"))
.dependsOn(xml.jvm, xmlScala.jvm, csv.jvm, cbor.jvm)
.dependsOn(xml.jvm, xmlScala.jvm, csv.jvm, cbor.jvm, json.jvm)
.settings(
libraryDependencies ++= Seq(
"io.circe" %%% "circe-generic" % "0.14.5",
Expand Down
55 changes: 55 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,58 @@ curl -s -X "POST" "http://localhost:8080/csv/toCbor" \

Then copy the output to [https://cbor.me](https://cbor.me) or a similar CBOR viewer. Make sure to view as `cborseq` otherwise the output will be truncated.



## http4s-fs2-data-json

Provides basic support for parsing and encoding `fs2.data.json.Token` streams that can be handled in a streaming fashion
using the pipes and builders `fs2-data` provides.

```scala
libraryDependencies += "org.http4s" %% "http4s-fs2-data-json" % "@VERSION@"
```

### Example

This example consumes a JSON input and returns it pretty printed.

```scala mdoc
import cats.effect.Async
import org.http4s.{EntityDecoder, EntityEncoder, HttpRoutes}
import org.http4s.dsl.Http4sDsl
import fs2.Stream
import fs2.data.json.Token

class JsonHttpEndpoint[F[_]](implicit F: Async[F]) extends Http4sDsl[F] {

private implicit val payloadDecoder: EntityDecoder[F, Stream[F, Token]] =
org.http4s.fs2data.json.jsonTokensDecoder

private implicit val payloadEncoder: EntityEncoder[F, Stream[F, Token]] =
org.http4s.fs2data.json.jsonTokensEncoder(prettyPrint = true)

val service: HttpRoutes[F] = HttpRoutes.of {
case req @ POST -> Root / "prettyJson" =>
Ok(Stream.force(req.as[Stream[F, Token]]))
}
}
```

You can try yourself with this snippet:

```shell
curl -s -X "POST" "http://localhost:8080/prettyJson" \
-H 'Content-Type: text/json; charset=utf-8' \
-d '{"a":2024,"b":[true,false],"c":{"d":"e"},"d":1}'
{
"a": 2024,
"b": [
true,
false
],
"c": {
"d": "e"
},
"d": 1
}
```
66 changes: 66 additions & 0 deletions json/src/main/scala/org/http4s/fs2data/json/JsonInstances.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023 http4s.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.http4s
package fs2data.json

import cats.data.NonEmptyList
import cats.effect.Concurrent
import cats.syntax.applicative._
import cats.syntax.monadError._
import fs2.data.json._
import fs2.Stream
import cats.syntax.show._
import org.http4s.Charset.`UTF-8`
import org.http4s.headers.{`Content-Type`, `Transfer-Encoding`}

trait JsonInstances {

implicit def jsonTokensDecoder[F[_]: Concurrent]: EntityDecoder[F, Stream[F, Token]] =
EntityDecoder.decodeBy(MediaType.application.json) { msg =>
DecodeResult.successT(
msg.bodyText
.through(tokens)
.adaptError { case ex: JsonException =>
MalformedMessageBodyFailure(
s"Invalid Json (${ex.context.fold("No context")(jc => jc.show)}): ${ex.msg}",
Some(ex),
)
}
)
}

def jsonTokensEncoder[F[_]](prettyPrint: Boolean)(implicit
charset: Charset = `UTF-8`
): EntityEncoder[F, Stream[F, Token]] = EntityEncoder.encodeBy(
Headers(
`Content-Type`(MediaType.application.json).withCharset(charset),
`Transfer-Encoding`(TransferCoding.chunked.pure[NonEmptyList]),
)
) { tokens =>
Entity(
tokens
.through(
if (prettyPrint) render.pretty() else render.compact
)
.through(fs2.text.encode[F](charset.nioCharset))
)
}

implicit def jsonTokensEncoder[F[_]]: EntityEncoder[F, Stream[F, Token]] =
jsonTokensEncoder(prettyPrint = false)

}
19 changes: 19 additions & 0 deletions json/src/main/scala/org/http4s/fs2data/json/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2023 http4s.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.http4s.fs2data

package object json extends JsonInstances
55 changes: 55 additions & 0 deletions json/src/test/scala/org/http4s/fs2data/json/JsonEventSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.http4s.fs2data.json

import cats.effect.IO
import cats.syntax.all._
import fs2.Stream
import fs2.data.json._
import fs2.data.json.literals.JsonInterpolator
import munit.CatsEffectSuite
import munit.ScalaCheckEffectSuite
import org.http4s.EntityDecoder
import org.http4s.Request

class JsonEventSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
test("round-trip Json") {
val in = json"""{"a": 1, "b": [true, false, null], "c": {"d": "e"}, "d": 1.2e3, "b": null}"""
Stream
.force(Request[IO]().withEntity(in.lift[IO]).as[Stream[IO, Token]])
.compile
.toList
.map(_.asRight[Throwable])
.assertEquals(in.toList)
}

test("round-trip Json string") {
val in = """{"a":1,"b":[true,false,null],"c":{"d":"e"},"d":1.2e3,"b":null}"""
Request[IO]()
.withEntity(in)
.as[Stream[IO, Token]]
.map(Request[IO]().withEntity(_))
.flatMap(EntityDecoder.text[IO].decode(_, false).value)
.assertEquals(Right(in))
}

test("round-trip Json pretty printing") {
val in = """{
| "a": 1,
| "b": [
| true,
| false,
| null
| ],
| "c": {
| "d": "e"
| },
| "d": 1.2e3,
| "b": null
|}""".stripMargin
Request[IO]()
.withEntity(in)
.as[Stream[IO, Token]]
.map(Request[IO]().withEntity(_)(jsonTokensEncoder[IO](prettyPrint = true)))
.flatMap(EntityDecoder.text[IO].decode(_, false).value)
.assertEquals(Right(in))
}
}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.9.4
sbt.version=1.9.8
6 changes: 3 additions & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.14.13")
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.16.2")

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0")

addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")

addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,10 @@ object generators {
n <- Gen.poisson(5)
s <- Gen.stringOfN(n, Gen.oneOf(char))
// Text may not contain these two in literal form, §2.4 of XML syntax
r = s.replace("&", "&amp;").replace("<", "&lt;")
// We replace them by empty strings instead of their quoted versions because Scala XML fails the roundtrip
// Relates to https://github.com/scala/scala-xml/issues/57
// Works around https://github.com/http4s/http4s-fs2-data/issues/88
r = s.replace("&", "").replace("<", "")
} yield Text(r)

val genComment: Gen[Comment] =
Expand Down

0 comments on commit 8943f93

Please sign in to comment.