Skip to content

Commit

Permalink
Merge pull request #34 from dylemma/dev/v0.10
Browse files Browse the repository at this point in the history
Split cats-effect and fs2 dependencies out of Core, reduce typeclass usage
  • Loading branch information
dylemma authored Jul 1, 2022
2 parents 70560ff + ff7df48 commit 8c4e9df
Show file tree
Hide file tree
Showing 62 changed files with 1,391 additions and 1,605 deletions.
42 changes: 31 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
ThisBuild / organization := "io.dylemma"
ThisBuild / version := "0.9.2"
ThisBuild / version := "0.10.0"
ThisBuild / scalaVersion := "2.13.0"
ThisBuild / crossScalaVersions := Seq("2.12.10", "2.13.5", "3.0.0")
ThisBuild / crossScalaVersions := Seq("2.12.16", "2.13.8", "3.1.3")
ThisBuild / scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature", "-language:higherKinds")
ThisBuild / scalacOptions ++= (scalaBinaryVersion.value match {
case "2.12" => Seq("-Ypartial-unification")
case _ => Nil
})

lazy val catsCore = "org.typelevel" %% "cats-core" % "2.6.1"
lazy val catsEffect = "org.typelevel" %% "cats-effect" % "3.1.1"
lazy val fs2Core = "co.fs2" %% "fs2-core" % "3.0.3"
lazy val fs2DataJson = "org.gnieh" %% "fs2-data-json" % "1.0.0-RC3"
lazy val fs2DataXml = "org.gnieh" %% "fs2-data-xml" % "1.0.0-RC3"
lazy val catsCore = "org.typelevel" %% "cats-core" % "2.8.0"
lazy val catsEffect = "org.typelevel" %% "cats-effect" % "3.3.13"
lazy val fs2Core = "co.fs2" %% "fs2-core" % "3.2.7"
lazy val fs2Io = "co.fs2" %% "fs2-io" % "3.2.7"
lazy val fs2DataJson = "org.gnieh" %% "fs2-data-json" % "1.4.1"
lazy val fs2DataXml = "org.gnieh" %% "fs2-data-xml" % "1.4.1"
lazy val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % "2.12.3"
lazy val typeName = "org.tpolecat" %% "typename" % "1.0.0"

Expand All @@ -29,14 +30,24 @@ lazy val core = (project in file("core"))
.settings(testSettings: _*)
.settings(apiDocSettings: _*)
.settings(publishingSettings: _*)
.settings(libraryDependencies ++= Seq(catsCore, catsEffect, fs2Core, typeName))
.settings(libraryDependencies ++= Seq(catsCore, typeName))

lazy val coreFs2 = (project in file("core-fs2"))
.settings(name := "spac-interop-fs2")
.settings(testSettings: _*)
.settings(apiDocSettings: _*)
.settings(publishingSettings: _*)
.settings(libraryDependencies ++= Seq(catsCore, catsEffect, fs2Core))
.dependsOn(core % "compile->compile;test->test")

lazy val xml = (project in file("xml"))
.settings(name := "xml-spac")
.settings(testSettings: _*)
.settings(apiDocSettings: _*)
.settings(publishingSettings: _*)
.dependsOn(core % "compile->compile;test->test")
.dependsOn(coreFs2 % "test->test")
.settings(libraryDependencies ++= Seq(catsEffect, fs2Core).map(_ % Test))

lazy val xmlJavax = (project in file("xml-javax"))
.settings(name := "xml-spac-javax")
Expand All @@ -58,6 +69,7 @@ lazy val json = (project in file("json"))
.settings(testSettings: _*)
.settings(apiDocSettings: _*)
.settings(publishingSettings: _*)
.dependsOn(coreFs2 % "test->test")
.dependsOn(core % "compile->compile;test->test")

lazy val jsonJackson = (project in file("json-jackson"))
Expand All @@ -74,19 +86,21 @@ lazy val jsonFs2Data = (project in file("json-fs2-data"))
.settings(apiDocSettings: _*)
.settings(publishingSettings: _*)
.settings(libraryDependencies += fs2DataJson)
.dependsOn(coreFs2)
.dependsOn(json % "compile->compile;test->test")

lazy val examples = (project in file("examples"))
.settings(
publish := {},
publish / skip := true,
libraryDependencies ++= Seq(catsEffect, fs2Core),
libraryDependencies ++= Seq(catsEffect, fs2Core, fs2Io),
)
.dependsOn(core, xml, xmlJavax, xmlFs2Data)
.dependsOn(core, coreFs2, xml, xmlJavax, xmlFs2Data)

lazy val root = (project in file("."))
.aggregate(
core,
coreFs2,
examples,
xml, xmlFs2Data, xmlJavax,
json, jsonJackson, jsonFs2Data
Expand Down Expand Up @@ -128,6 +142,7 @@ lazy val apiDocSettings = Seq(
"-groups",
"-implicits",
s"-implicits-hide:${ classesForHiddenConversions.mkString(",") }",
"-skip-packages", scaladocIgnoredPackages.mkString(":"),
"-sourcepath", sourcePath,
"-doc-source-url", sourceUrl
)
Expand All @@ -143,7 +158,6 @@ lazy val apiDocSettings = Seq(
lazy val classesForHiddenConversions = Seq(
// these end up being added to literally every class,
// despite the fact that they should never actually be applied to a spac class
"io.dylemma.spac.SourceToPullable",
"io.dylemma.spac.xml.elem",

// for some reason, specifying any `-implicits-hide` flag to the scaladoc task
Expand All @@ -155,6 +169,12 @@ lazy val classesForHiddenConversions = Seq(
"scala.Predef.StringFormat",
"scala.Predef.ArrowAssoc"
)
lazy val scaladocIgnoredPackages = Seq(
"io.dylemma.spac.impl",
"io.dylemma.spac.xml.impl",
"io.dylemma.spac.json.impl",
"io.dylemma.spac.interop.fs2.impl"
)

lazy val publishingSettings = Seq(
publishMavenStyle := true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package io.dylemma.spac
package io.dylemma.spac.interop.fs2
package impl

import fs2.{Pipe, Pull, Stream}
import _root_.fs2.{Pipe, Pull, Stream}
import io.dylemma.spac.{Parser, SpacTraceElement}

object ParserToPipe {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.dylemma.spac
package impl
package io.dylemma.spac.interop.fs2.impl

import fs2.{Chunk, Pipe, Pull, Stream}
import _root_.fs2.{Chunk, Pipe, Pull, Stream}
import io.dylemma.spac.{SpacTraceElement, Transformer}

import scala.collection.immutable.VectorBuilder

Expand Down
132 changes: 132 additions & 0 deletions core-fs2/src/main/scala/io/dylemma/spac/interop/fs2/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.dylemma.spac.interop

import _root_.fs2.{Chunk, Compiler, Pipe, Stream}
import cats.MonadError
import cats.effect.{MonadCancel, Resource, Sync}
import io.dylemma.spac._
import io.dylemma.spac.interop.fs2.impl._

/** Provides implicits to allow for interop between the core SPaC classes and fs2 / cats-effect.
*
* - `Parser` gets `toPipe` and `parseF`
* - `Transformer` gets `toPipe`
* - `Source` gets `toResource` and `toStream`
*/
package object fs2 {

implicit def unconsableForFs2Chunk: Unconsable[Chunk] = new Unconsable[Chunk] {
def uncons[A](chunk: Chunk[A]) = {
if (chunk.isEmpty) None
else {
val (headChunk, tail) = chunk.splitAt(1)
headChunk.head.map(_ -> tail)
}
}
}

/** Since `Parser` is by design a stream consumer, we can provide the `parseF` helper which
* consumes a `fs2.Stream` in an effectful way. We can also provide the `toPipe` method, which transforms
* an input `fs2.Stream` to a new stream which emits exactly one value or raises an error.
*
* @param parser
* @tparam In
* @tparam Out
*/
implicit class ParserFs2Ops[In, Out](private val parser: Parser[In, Out]) extends AnyVal {
/** Convert this parser to a FS2 "Pipe".
* The resulting pipe will forward inputs from the upstream into this parser,
* emitting a single value to the downstream when this parser finishes.
* Since a `Parser` may abort early (e.g. with `Parser.first`),
* the pipe may not pull the entire input stream.
*
* @param pos
* @tparam F
* @return
* @group consumers
*/
def toPipe[F[_]](implicit pos: CallerPos): Pipe[F, In, Out] = {
ParserToPipe(parser, SpacTraceElement.InParse("parser", "toPipe", pos))
}

/** Convenience for `stream.through(parser.toPipe).compile.lastOrError`.
*
* Uses this parser to pull from the `stream` until the parser emits a result or the stream is depleted.
*
* @param stream The stream of events (of type `In`) to consume
* @param compiler The fs2 stream compiler
* @param G Evidence that the `G` effect type can raise Throwables as errors
* @param pos call-point information used to generate the top SpacTraceElement for error handling
* @tparam F The stream effect type, e.g. `cats.effect.IO`
* @tparam G The stream-compiler output type. Usually the same as `F`
* @return
*/
def parseF[F[_], G[_]](stream: Stream[F, In])(implicit compiler: Compiler[F, G], G: MonadError[G, Throwable], pos: CallerPos): G[Out] = {
stream.through(ParserToPipe(parser, SpacTraceElement.InParse("parser", "parseF", pos))).compile.lastOrError
}
}

/** Since a `Transformer` is by design a stream transformation, we can naturally provide a conversion from `Transformer`
* to `fs2.Pipe`
*
* @param transformer
* @tparam In
* @tparam Out
*/
implicit class TransformerFs2Ops[In, Out](private val transformer: Transformer[In, Out]) extends AnyVal {
/** Convert this transformer to a `Pipe` which will apply this transformer's logic to an fs2 `Stream`.
*
* @param pos Captures the caller filename and line number, used to fill in the 'spac trace' if the parser throws an exception
* @tparam F Effect type for the Pipe/Stream
* @return An `fs2.Pipe[F, In, Out]` that will apply this transformer's logic
* @group transform
*/
def toPipe[F[_]](implicit pos: CallerPos): Pipe[F, In, Out] = TransformerToPipe(transformer, SpacTraceElement.InParse("transformer", "toPipe", pos))
}

/** Since `Source` is a synchronous-only encoding of the `Resource` pattern, it can
* be converted to a `cats.effect.Resource` by suspending its `open` and `close`
* operations in a Sync effect type `F`, yielding an `Iterator[A]` as its value.
*
* This can be taken a step further by lifting that `Resource` to a `fs2.Stream`
* and wrapping the provided Iterator as a stream, to treat the whole `Source[A]`
* as a `fs2.Stream[F, A]`
*
* @param source
* @tparam A
*/
implicit class SourceFs2Ops[A](private val source: Source[A]) extends AnyVal {

/** Upgrades this `Source` to a cats-effect `Resource` of the given effect type `F`.
* The open and close operations of the underlying source are assumed to be blocking,
* so they are wrapped with `Sync[F].blocking { ... }`.
*
* @tparam F The effect type
* @return A new Resource which delegates to this Source's `open` method
*/
def toResource[F[_]: Sync]: Resource[F, Iterator[A]] = {
val F = Sync[F]
Resource(F.blocking {
val (itr, close) = source.open()
val closeF = F.blocking { close() }
itr -> closeF
})
}

/** Converts this `Source` to an fs2 `Stream` in the given effect type `F`.
*
* Uses [[toResource]] to encapsulate the underlying open/close operation,
* and uses `Stream.fromBlockingIterator` to wrap the underlying Iterator provided by the Source.
* The underlying Iterator is assumed to use blocking operations internally since typically
* the Iterator would be backed by something like a `java.io.InputStream`.
*
* @param chunkSize The number of times the underlying Iterator's `next` should be called, per blocking step
* @param F Sync[F] typeclass instance
* @param FM MonadCancel[F, _] typeclass instance
* @tparam F The effect type
* @return A Stream over the data provided by the underlying Source
*/
def toStream[F[_]](chunkSize: Int = 32)(implicit F: Sync[F], FM: MonadCancel[F, _]): Stream[F, A] = {
Stream.resource(toResource[F]).flatMap(Stream.fromBlockingIterator[F](_, chunkSize))
}
}
}
2 changes: 1 addition & 1 deletion core/src/main/scala-3/io.dylemma.spac/CallerPos.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object CallerPos {
def deriveCallerPos(using quotes: Quotes): Expr[CallerPos] = {
import quotes.reflect._
val pos = Position.ofMacroExpansion
val filename = Expr { pos.sourceFile.jpath.getFileName.toString }
val filename = Expr { pos.sourceFile.name }
// note: scala 3 uses 0-based line numbers - https://github.com/lampepfl/dotty/discussions/12728
val line = Expr { pos.startLine + 1 }
'{ CallerPos(${filename}, ${line}) }
Expand Down
21 changes: 0 additions & 21 deletions core/src/main/scala/io/dylemma/spac/ChunkSize.scala

This file was deleted.

75 changes: 0 additions & 75 deletions core/src/main/scala/io/dylemma/spac/Parsable.scala

This file was deleted.

Loading

0 comments on commit 8c4e9df

Please sign in to comment.