Skip to content

Commit

Permalink
Add scalafix and scalafmt plugins and reformat files
Browse files Browse the repository at this point in the history
  • Loading branch information
voidcontext committed May 13, 2020
1 parent b1dcdf2 commit 9b880ae
Show file tree
Hide file tree
Showing 16 changed files with 192 additions and 128 deletions.
26 changes: 26 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
rules = [
RemoveUnused,
DisableSyntax,
LeakingImplicitClassVal,
NoValInForComprehension,
ProcedureSyntax,
SortImports
]

DisableSyntax.noVars = true
DisableSyntax.noThrows = true
DisableSyntax.noNulls = true
DisableSyntax.noReturns = true
DisableSyntax.noWhileLoops = true
DisableSyntax.noAsInstanceOf = true
DisableSyntax.noIsIstanceOf = true
DisableSyntax.noXml = true
DisableSyntax.noFinalize = true
DisableSyntax.noValPatterns = true
DisableSyntax.noUniversalEquality = true

SortImports.blocks = [
"*",
"scala.",
"java.",
]
15 changes: 15 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version = 2.4.1
maxColumn = 120
align.openParenCallSite = false
align.openParenDefnSite = false
align.tokens = [
{ code = "=>" , owner = "Case" },
{ code = "<-" , owner = "Enumerator.Generator" },
{ code = "%" },
{ code = "%%" }
]
danglingParentheses = true
continuationIndent.defnSite = 2
includeCurlyBraceInSelectChains = false
docstrings = JavaDoc
rewrite.rules = [SortImports]
36 changes: 21 additions & 15 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ThisBuild / name := "fetch-file"
ThisBuild / scalaVersion := "2.13.1"
ThisBuild / organization := "com.gaborpihaj"
ThisBuild / dynverSonatypeSnapshots := true
ThisBuild / scalafixDependencies += "com.nequissimus" %% "sort-imports" % "0.3.2"

ThisBuild / publishTo := sonatypePublishToBundle.value

Expand All @@ -16,46 +17,51 @@ val scalaTestVersion = "3.1.1"
lazy val publishSettings = List(
licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
publishMavenStyle := true,
sonatypeProjectHosting := Some(GitHubHosting("voidcontext", "fetch-file", "gabor.pihaj@gmail.com")),
sonatypeProjectHosting := Some(GitHubHosting("voidcontext", "fetch-file", "gabor.pihaj@gmail.com"))
)

lazy val fetchfile = (project in file("fetch-file"))
.settings(publishSettings)
.settings(
name := "fetch-file",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % catsEffectVersion,
"co.fs2" %% "fs2-core" % fs2Version,
"co.fs2" %% "fs2-io" % fs2Version,

"org.scalatest" %% "scalatest" % scalaTestVersion % Test,
"org.scalacheck" %% "scalacheck" % "1.14.1" % Test,
"org.typelevel" %% "cats-effect" % catsEffectVersion,
"co.fs2" %% "fs2-core" % fs2Version,
"co.fs2" %% "fs2-io" % fs2Version,
"org.scalatest" %% "scalatest" % scalaTestVersion % Test,
"org.scalacheck" %% "scalacheck" % "1.14.1" % Test,
"org.scalatestplus" %% "scalatestplus-scalacheck" % "3.1.0.0-RC2" % Test
),
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full)
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full),
addCompilerPlugin(scalafixSemanticdb)
)

lazy val fetchfileHttp4s = (project in file("fetch-file-http4s"))
.settings(publishSettings)
.settings(
name := "fetch-file-http4s",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % catsEffectVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-blaze-client" % http4sVersion,

"org.scalatest" %% "scalatest" % scalaTestVersion % Test,
"org.typelevel" %% "cats-effect" % catsEffectVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-blaze-client" % http4sVersion,
"org.scalatest" %% "scalatest" % scalaTestVersion % Test
),
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full)
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full),
addCompilerPlugin(scalafixSemanticdb)
)
.dependsOn(fetchfile)

lazy val examples = (project in file("examples"))
.settings(
skip in publish := true,
addCompilerPlugin(scalafixSemanticdb)
)
.dependsOn(fetchfile)

lazy val root = (project in file("."))
.settings(
skip in publish := true,
skip in publish := true
)
.aggregate(fetchfile, fetchfileHttp4s, examples)

addCommandAlias("prePush", ";scalafix ;test:scalafix ;scalafmtAll ;scalafmtSbt; reload ;clean ;test")
2 changes: 1 addition & 1 deletion examples/src/main/scala/vdx/fetchfile/examples/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import cats.effect._
import cats.syntax.functor._
import vdx.fetchfile._

import java.net.URL
import java.io.{File, FileOutputStream}
import java.net.URL

object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import org.http4s.headers.`Content-Length`
import org.http4s.{Headers, Status}
import vdx.fetchfile.Downloader.ContentLength


object Http4sClient {
def apply[F[_]: MonadError[*[_], Throwable]](client: Client[F]): HttpClient[F] =
url => sink =>
client.get(url.toString()) {
case Status.Successful(r) => sink(ContentLength(contentLength(r.headers)), r.body)
case _ => MonadError[F, Throwable].raiseError(new HttpClientException("HTTP call failed."))
}
url =>
sink =>
client.get(url.toString()) {
case Status.Successful(r) => sink(ContentLength(contentLength(r.headers)), r.body)
case _ => MonadError[F, Throwable].raiseError(new HttpClientException("HTTP call failed."))
}

private[this] def contentLength(headers: Headers): Long =
headers.get(`Content-Length`).map(_.length).getOrElse(0L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package http4s
import cats.effect._
import cats.syntax.semigroupal._
import org.http4s.client.blaze.BlazeClientBuilder
import org.scalatest.EitherValues
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

Expand All @@ -13,32 +14,35 @@ import scala.io.Source
import java.io.ByteArrayOutputStream
import java.net.URL
import java.security.MessageDigest
import org.scalatest.EitherValues

class Http4sClientSpec extends AnyFlatSpec with Matchers with EitherValues {

implicit val cs: ContextShift[IO] = IO.contextShift(global)

"Http4sClient" should "download the file correctly via the http4s client" in {

val downloadedBytes = (Blocker[IO].product(BlazeClientBuilder[IO](global).resource).use {
case (blocker, client) =>
implicit val backend = Http4sClient[IO](client)
val downloader = Downloader[IO](blocker)
val out = new ByteArrayOutputStream()
for {
_ <- downloader.fetch(
new URL("http://localhost:8088/100MB.bin"),
Resource.fromAutoCloseable(IO.delay(out)),
)
content <- IO.delay(out.toByteArray())
} yield content
}).unsafeRunSync()
val downloadedBytes = (Blocker[IO]
.product(BlazeClientBuilder[IO](global).resource)
.use {
case (blocker, client) =>
implicit val backend = Http4sClient[IO](client)
val downloader = Downloader[IO](blocker)
val out = new ByteArrayOutputStream()
for {
_ <- downloader.fetch(
new URL("http://localhost:8088/100MB.bin"),
Resource.fromAutoCloseable(IO.delay(out))
)
content <- IO.delay(out.toByteArray())
} yield content
})
.unsafeRunSync()

downloadedBytes.length should be(1024 * 1024 * 100)
val expectedShaSum = Source.fromFile("docker/static-files/100MB.bin.shasum").mkString.trim()
val expectedShaSum = Source.fromFile("docker/static-files/100MB.bin.sha256").mkString.trim()

val shaSum = MessageDigest.getInstance("SHA-1")
val shaSum = MessageDigest
.getInstance("SHA-256")
.digest(downloadedBytes)
.map("%02x".format(_))
.mkString
Expand All @@ -47,18 +51,22 @@ class Http4sClientSpec extends AnyFlatSpec with Matchers with EitherValues {
}

it should "raise an error when the HTTP request is not successful" in {
val attempt = (Blocker[IO].product(BlazeClientBuilder[IO](global).resource).use {
case (blocker, client) =>
implicit val backend = Http4sClient[IO](client)

val downloader = Downloader[IO](blocker)
val out = new ByteArrayOutputStream()

downloader.fetch(
new URL("http://localhost:8088/nonexsitent"),
Resource.fromAutoCloseable(IO.delay(out)),
)
}).attempt.unsafeRunSync()
val attempt = (Blocker[IO]
.product(BlazeClientBuilder[IO](global).resource)
.use {
case (blocker, client) =>
implicit val backend = Http4sClient[IO](client)

val downloader = Downloader[IO](blocker)
val out = new ByteArrayOutputStream()

downloader.fetch(
new URL("http://localhost:8088/nonexsitent"),
Resource.fromAutoCloseable(IO.delay(out))
)
})
.attempt
.unsafeRunSync()

attempt.left.value should be(an[HttpClientException])

Expand Down
35 changes: 19 additions & 16 deletions fetch-file/src/main/scala/vdx/fetchfile/Downloader.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package vdx.fetchfile

import cats.effect._
import cats.syntax.eq._
import cats.instances.string._
import cats.syntax.eq._
import cats.syntax.functor._
import fs2.{Pipe, Stream}
import fs2.io.writeOutputStream
import fs2.{Pipe, Stream}

import java.io.OutputStream
import java.net.URL
Expand All @@ -23,6 +23,7 @@ import java.net.URL
* }}}
*/
trait Downloader[F[_]] {

/**
* Fetches the given URL and popuplates the given output stream.
*/
Expand All @@ -37,12 +38,13 @@ object Downloader {
*/
def apply[F[_]: Concurrent: ContextShift](
ec: Blocker,
progress: ContentLength => Pipe[F, Byte, Unit] = Progress.noop[F],
progress: ContentLength => Pipe[F, Byte, Unit] = Progress.noop[F]
)(implicit client: HttpClient[F]): Downloader[F] = new Downloader[F] {
def fetch(url: URL, out: Resource[F, OutputStream], sha256Sum: Option[String] = None): F[Unit] =
out.use { outStream =>
client(url) { (contentLength, body) =>
body.observe(progress(contentLength))
body
.observe(progress(contentLength))
// The writeOutputStream pipe returns Unit so it is safe to write the final output using observe
.observe(writeOutputStream[F](Concurrent[F].delay(outStream), ec))
.through(maybeCompareSHA(sha256Sum))
Expand All @@ -53,18 +55,19 @@ object Downloader {

def maybeCompareSHA(sha256: Option[String]): Pipe[F, Byte, Unit] =
stream =>
sha256.map[Stream[F, Unit]] { expectedSHA =>
Stream.eval(
// We'll compute the sh256 hash of the downloaded file
stream.through(fs2.hash.sha256)
.compile
.toVector
).flatMap { hashBytes =>
val hash = hashBytes.map("%02x".format(_)).mkString
if (hash === expectedSHA.toLowerCase()) Stream.emit(()).covary[F]
else Stream.raiseError(new Exception(s"Sha256 sum doesn't match (expected: $expectedSHA, got: $hash)"))
sha256
.map[Stream[F, Unit]] { expectedSHA =>
Stream
.eval(
// We'll compute the sh256 hash of the downloaded file
stream.through(fs2.hash.sha256).compile.toVector
)
.flatMap { hashBytes =>
val hash = hashBytes.map("%02x".format(_)).mkString
if (hash === expectedSHA.toLowerCase()) Stream.emit(()).covary[F]
else Stream.raiseError(new Exception(s"Sha256 sum doesn't match (expected: $expectedSHA, got: $hash)"))
}
}
}
.getOrElse(stream.void)
.getOrElse(stream.void)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package vdx.fetchfile

final case class HttpClientException(
private val message: String,
private val cause: Exception = null
private val cause: Exception = None.orNull
) extends Exception(message, cause)
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@

package vdx.fetchfile

import cats.effect._
import fs2.io.{readInputStream}

import java.net.{HttpURLConnection, URL}
import java.io.InputStream
import vdx.fetchfile.Downloader.ContentLength

import java.io.InputStream
import java.net.{HttpURLConnection, URL}

/**
* A HttpClient implementation based on java.net.HttpURLConnection.
*/
object HttpURLConnectionClient {

/**
* Creates a HttpClient instance that is using java.net.HttpUrlConnection to make a HTTP request.
*
* The evaluation of the blocking HTTP call and the fs2.Stream creation from the java InputStream
* is using the given execution context wrapped in Blocker.
*/
def apply[F[_]: Sync: ContextShift](blocker: Blocker, chunkSize: Int): HttpClient[F] =
url => sink =>
makeConnectionResource(url, blocker)
.map {
case (contentLength, inputStream) =>
ContentLength(contentLength.toLong) -> readInputStream(Sync[F].delay(inputStream), chunkSize, blocker)
}
.use(sink.tupled)

url =>
sink =>
makeConnectionResource(url, blocker).map {
case (contentLength, inputStream) =>
ContentLength(contentLength.toLong) -> readInputStream(Sync[F].delay(inputStream), chunkSize, blocker)
}.use(sink.tupled)

private[this] def makeConnectionResource[F[_]: Sync: ContextShift](
url: URL,
blocker: Blocker
): Resource[F, (Int, InputStream)] =
Resource.make(makeConnection(url, blocker)) { case (_, inStream) => Sync[F].delay(inStream.close())}
Resource.make(makeConnection(url, blocker)) { case (_, inStream) => Sync[F].delay(inStream.close()) }

@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf"))
private[this] def makeConnection[F[_]: Sync: ContextShift](url: URL, blocker: Blocker): F[(Int, InputStream)] =
ContextShift[F].blockOn(blocker)(
Sync[F].delay {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ trait MonotonicClock {
}

object MonotonicClock {

/**
* Simple clock implementation based on System.nanoTime()
*/
def system(): MonotonicClock = new MonotonicClock {
def nanoTime(): Long = System.nanoTime()
}
}

0 comments on commit 9b880ae

Please sign in to comment.