http4s-fs2-data

http4s-fs2-data provides a set of integration libraries that integrate http4s with the streaming parsers offered by fs2-data.

http4s-fs2-data-xml

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

libraryDependencies += "org.http4s" %% "http4s-fs2-data-xml" % "0.4.0"

http4s-fs2-data-xml-scala

Provides additional integration with scala-xml to work directly with its Document ans Elem types. To some extent, this module is a drop-in replacement for the http4s-scala-xml module, but it provides additional streaming capabilities scala-xml doesn't.

libraryDependencies += "org.http4s" %% "http4s-fs2-data-xml-scala" % "0.4.0"

Example

import cats.effect.Async
import cats.syntax.flatMap._
import io.circe.generic.auto._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
import org.http4s.{ApiVersion => _, _}

import scala.xml._

val ApiVersion = "v1"
// ApiVersion: String = "v1"

// Docs: http://http4s.org/latest/entity/
class JsonXmlHttpEndpoint[F[_]](implicit F: Async[F]) extends Http4sDsl[F] {
  private case class Person(name: String, age: Int)

  /** XML Example for Person:
    *
    * <person>
    *   <name>gvolpe</name>
    *   <age>30</age>
    * </person>
    */
  private object Person {
    def fromXml(elem: Document): Person = {
      val name = (elem \\ "name").text
      val age = (elem \\ "age").text
      Person(name, age.toInt)
    }
  }

  private def personXmlDecoder: EntityDecoder[F, Person] =
    org.http4s.fs2data.xml.scalaxml.xmlDocumentDecoder.map(Person.fromXml)

  implicit private def jsonXmlDecoder: EntityDecoder[F, Person] =
    jsonOf[F, Person].orElse(personXmlDecoder)

  val service: HttpRoutes[F] = HttpRoutes.of {
    case GET -> Root / ApiVersion / "media" =>
      Ok(
        "Send either json or xml via POST method. Eg: \n{\n  \"name\": \"gvolpe\",\n  \"age\": 30\n}\n or \n <person>\n  <name>gvolpe</name>\n  <age>30</age>\n</person>"
      )

    case req @ POST -> Root / ApiVersion / "media" =>
      req.as[Person].flatMap { person =>
        Ok(s"Successfully decoded person: ${person.name}")
      }
  }
}

http4s-fs2-data-csv

Provides basic support for parsing and encoding CSV streams that can be handled in a streaming fashion using the pipes fs2-data provides. Note that this integration does not expose any implicits, but due to the varity of CSV styles (comma/semicolon/tab for example) it exposes methods that lift the collection of CSV pipes fs2-data offers to HTTP level so that all parsing options can conveniently be specified. Implicit encoder and decoder can easily be built on top of this if the options are fixed with a certain context.

Example

This example parses a CSV input with headers and outputs the parsed data as a JSON array.

import fs2.Stream
import fs2.data.csv._
import fs2.data.csv.generic.semiauto
import org.http4s.circe.CirceEntityEncoder._

class CsvStatsHttpEndpoint[F[_]](implicit F: Async[F]) extends Http4sDsl[F] {
  private case class Person(name: String, age: Int)
  private object Person {
    implicit val decoder: CsvRowDecoder[Person, String] = semiauto.deriveCsvRowDecoder
  }
  
  private implicit val personDecoder: EntityDecoder[F, Stream[F, Person]] =
    org.http4s.fs2data.csv.csvDecoderForPipe(decodeUsingHeaders[Person]())

  // or more generic if you have a couple of similar inputs
  // private implicit def genericDecoder[T](implicit T: CsvRowDecoder[T, String]): EntityDecoder[F, Stream[F, T]] =
  //   org.http4s.fs2data.csv.csvDecoderForPipe(decodeUsingHeaders[T]())

  val service: HttpRoutes[F] = HttpRoutes.of { 
    case req @ POST -> Root / "csv" / "toJson" =>
      Ok(Stream.force(req.as[Stream[F, Person]]).compile.toList)
  }
}

http4s-fs2-data-cbor

Provides basic support for parsing and encoding CBOR streams that can be handled in a streaming fashion, either treating the in/output as a stream itself or as a single value.

Example

This example consumes a CSV input, converts it to CBOR as a simple stream of arrays provides and returns it.

import fs2.Stream
import fs2.data.cbor.high.CborValue
import fs2.data.csv._
import org.http4s.fs2data.cbor._
import org.http4s.fs2data.csv._

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

  private implicit val decoder: EntityDecoder[F, Stream[F, Row]] = rowDecoder()
  
  val service: HttpRoutes[F] = HttpRoutes.of { 
    case req @ POST -> Root / "csv" / "toCbor" =>
      Ok(Stream.force(req.as[Stream[F, Row]]).map(toCbor))
  }
  
  private def toCbor(row: Row): CborValue =
    CborValue.Array(row.values.toList.map(CborValue.TextString(_)), indefinite = false)
}

You can try yourself with this snippet:

curl -s -X "POST" "http://localhost:8080/csv/toCbor" \
     -H 'Content-Type: text/csv; charset=utf-8' \
     -d $'1,Ene,Mene
2,Muh,!' | od -A n -t x1

Then copy the output to 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.

libraryDependencies += "org.http4s" %% "http4s-fs2-data-json" % "0.4.0"

Example

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

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:

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
}