Skip to content

Commit

Permalink
0.9.0 - support fs2v3
Browse files Browse the repository at this point in the history
  • Loading branch information
ScalaWilliam committed Jul 27, 2021
1 parent fcef26b commit 48efb0e
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name := "xs4s-root"
ThisBuild / scalaVersion := "2.13.6"
ThisBuild / crossScalaVersions := Seq("2.12.12", "2.13.6", "3.0.1")
ThisBuild / organization := "com.scalawilliam"
ThisBuild / version := "0.8.8"
ThisBuild / version := "0.9.0"
ThisBuild / resolvers += Resolver.JCenterRepository

lazy val root = (project in file("."))
Expand Down
47 changes: 47 additions & 0 deletions fs2v3/src/main/scala/xs4s/fs2compat/Fs2Syntax.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package xs4s.fs2compat

import _root_.fs2.{Pipe, Stream}
import cats.effect.{Resource, Sync}
import xs4s.XmlElementExtractor
import xs4s.generic.Scanner
import xs4s.syntax.core._

import javax.xml.stream.XMLEventReader
import javax.xml.stream.events.XMLEvent
import scala.language.higherKinds

/**
* Utilities to enhance xs4s interaction with FS2
*/
trait Fs2Syntax {

implicit class RichScanner[In, State, Out](scanner: Scanner[In, State, Out]) {

/** Create an FS2 Pipe from the scanner */
def fs2Pipe[F[_]]: Pipe[F, In, Out] =
_.scan(scanner.initial)(scanner.scan).map(scanner.collect).unNone
}

implicit class RichFs2StreamObj(obj: Stream.type) {

/** Create an XMLEvent Stream from an XMLEventReader */
def xmlEventStream[F[_]: Sync](
xmlEventReader: Resource[F, XMLEventReader],
chunkSize: Int = 1): Stream[F, XMLEvent] =
Stream
.resource(xmlEventReader)
.flatMap(
reader =>
Stream
.fromBlockingIterator[F]
.apply[XMLEvent](reader.toIterator, chunkSize))
}

implicit class RichXmlElementExtractor[O](
xmlElementExtractor: XmlElementExtractor[O]) {
def toFs2PipeThrowError[F[_]]: Pipe[F, XMLEvent, O] =
xmlElementExtractor.scannerThrowingOnError.fs2Pipe[F]
def toFs2PipeIncludeError[F[_]]: Pipe[F, XMLEvent, Either[Throwable, O]] =
xmlElementExtractor.scannerEitherOnError.fs2Pipe[F]
}
}
32 changes: 32 additions & 0 deletions fs2v3/src/main/scala/xs4s/fs2compat/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package xs4s

import cats.effect.{Async, Resource, Sync}
import fs2._
import xs4s.syntax.fs2._

import javax.xml.stream.XMLInputFactory
import javax.xml.stream.events.XMLEvent
import scala.language.higherKinds

package object fs2compat {

/**
* Turns an FS2 Byte Stream into a stream of XMLEvent.
* It turns the source stream into an input stream,
* then creates a reader, and then creates a further stream
* from the Iterator that was obtained.
* */
def byteStreamToXmlEventStream[F[_]: Async](
xmlInputFactory: XMLInputFactory = defaultXmlInputFactory,
chunkSize: Int = 1)(implicit F: Sync[F]): Pipe[F, Byte, XMLEvent] =
byteStream =>
Stream
.resource(io.toInputStreamResource(byteStream))
.flatMap(
inputStream =>
Stream.xmlEventStream(
Resource.make(
F.delay(xmlInputFactory.createXMLEventReader(inputStream)))(
xmlEventReader => F.delay(xmlEventReader.close())),
chunkSize))
}
3 changes: 3 additions & 0 deletions fs2v3/src/main/scala/xs4s/syntax/fs2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package xs4s.syntax

object fs2 extends xs4s.fs2compat.Fs2Syntax {}
41 changes: 41 additions & 0 deletions fs2v3/src/test/scala/xs4s/fs2compat/Fs2CompatSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package xs4s.fs2compat

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import fs2.Stream
import org.scalatest.freespec.AnyFreeSpec
import xs4s.XmlElementExtractor
import xs4s.syntax.fs2.RichXmlElementExtractor

import scala.xml.Elem

final class Fs2CompatSpec extends AnyFreeSpec {
"It works" in {
val input =
s"""
|<items>
|<embedded><item>Embedded</item></embedded>
|<item>General</item>
|<embedded-twice><embedded-once><item>Doubly embedded</item></embedded-once></embedded-twice>
|<item><item>Nested</item></item>
|</items>
|
""".stripMargin

val anchorElementExtractor: XmlElementExtractor[Elem] =
XmlElementExtractor.filterElementsByName("item")

val textStream: Stream[IO, String] = fs2.Stream
.apply[IO, String](input)
.flatMap(str => fs2.Stream.emits(str.getBytes().toList))
.through(byteStreamToXmlEventStream[IO]())
.through(anchorElementExtractor.toFs2PipeThrowError)
.map(_.text)

assert(
textStream.compile.toList.unsafeRunSync() == List("Embedded",
"General",
"Doubly embedded",
"Nested"))
}
}

0 comments on commit 48efb0e

Please sign in to comment.