diff --git a/README.md b/README.md index bebab0c..0d2e4ee 100644 --- a/README.md +++ b/README.md @@ -3,52 +3,75 @@ [GS Open Source](https://garrity.co/oss.html) | [License (Apache 2.0)](./LICENSE) -Garrity Software standard types and operations. Provides a zero-dependency -collection of basic tools. +Garrity Software standard types and operations with minimal dependencies. - [Usage](#usage) - - [Dependency](#dependency) -- [Donate](#donate) +- [Core Module](#core-module) +- [I/O Module](#io-module) +- [Effect Module](#effect-module) ## Usage -### Dependency - This artifact is available in the Garrity Software Maven repository. ```scala externalResolvers += "Garrity Software Releases" at "https://maven.garrity.co/releases" -val GsStd: ModuleID = - "gs" %% "gs-std-v0" % "$VERSION" +// No Dependencies +val GsStdCore: ModuleID = + "gs" %% "gs-std-core-v0" % "$VERSION" + +// Depends on Cats Effect and Fs2 +val GsStdIO: ModuleID = + "gs" %% "gs-std-io-v0" % "$VERSION" + +// Depends on Cats Effect and Fs2 +val GsStdEffect: ModuleID = + "gs" %% "gs-std-effect-v0" % "$VERSION" ``` -## Types +## Core Module -- `Nat` -- `Size` -- `ByteCount` -- `Blob` -- `CreatedAt` -- `UpdatedAt` -- `SHA256` -- `MD5` -- `EncodedString` -- `B64` -- `B64Url` -- `Hex` +Provides standard types, wrappers, and tools. -## Tools +- Common semantic types (e.g. `CreatedAt`). +- Encoding and representing encoded data. +- Hashing algorithms and representing hashes. +- Reasoning about bytes and blobs. -- `Encoder` -- `Base64Encoder` -- `HexEncoder` -- `Decoder` -- `Base64Decoder` -- `HexDecoder` +## I/O Module -## Donate +### File Helpers -Enjoy this project or want to help me achieve my [goals](https://garrity.co)? -Consider [Donating to Pat on Ko-fi](https://ko-fi.com/gspfm). +`Files`: These provide support for reading and writing files. Provides wrapping +around `java.nio.file.Files`. + +### Resource Helpers + +These provide support for reading files from packaged resource directories +(e.g. `src/main/resources`). + +## Effect Module + +This module relies on the [Core Module](#core-module) and takes advantage of the +types defined therein. + +### Random Numbers + +`Rng[F[_]]`: Effectful random number generator with creation/testing +helpers. + +### Dates and Times + +`DateTimeProvider[F[_]]`: Effectful provider for dates and times. Provides an +implementation based on clock injection. Helps to decouple date/time use from +static methods. + +### Streaming Hash + +Provides support for streaming arbitrary bytes through different hashing +algorithms: + +- MD5 (`MD5Pipe[F[_]]`) +- SHA-256 (`SHA256Pipe[F[_]]`) diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/pipes/SHA256Pipe.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/pipes/SHA256Pipe.scala deleted file mode 100644 index 62636a3..0000000 --- a/modules/effect/src/main/scala/src/gs/std/v0/effect/pipes/SHA256Pipe.scala +++ /dev/null @@ -1,46 +0,0 @@ -package gs.std.v0.effect.pipes - -import cats.Applicative -import fs2.Chunk -import fs2.Pipe -import fs2.Pull -import fs2.Stream -import gs.std.v0.core.SHA256 -import java.security.MessageDigest - -class SHA256Pipe[F[_]: Applicative] extends Pipe[F, Byte, SHA256]: - - private def newDigest(): MessageDigest = - MessageDigest.getInstance(SHA256.Algorithm) - - private def append( - digest: MessageDigest, - data: Chunk[Byte] - ): F[Unit] = - Applicative[F].pure(digest.update(data.toArray)) - - def pull( - s: Stream[F, Byte], - digest: MessageDigest - ): Pull[F, SHA256, Unit] = - s.pull.uncons.flatMap { - case Some((head, tail)) => - // There is another chunk of data. - // Update the digest and then handle the next chunk. - Pull.eval(append(digest, head)) >> pull(tail, digest) - case None => - // The stream is empty. Emit a single chunk that contains the calculated - // hash. - Pull.output(Chunk(SHA256.fromBytes(digest.digest()))) >> Pull.done - } - - override def apply(v1: Stream[F, Byte]): Stream[F, SHA256] = - pull(v1, newDigest()).stream - -end SHA256Pipe - -object SHA256Pipe: - - def apply[F[_]: Applicative] = new SHA256Pipe[F] - -end SHA256Pipe diff --git a/modules/effect/src/test/scala/src/gs/std/v0/effect/pipes/SHA256PipeTests.scala b/modules/effect/src/test/scala/src/gs/std/v0/effect/pipes/SHA256PipeTests.scala deleted file mode 100644 index 19bd2a9..0000000 --- a/modules/effect/src/test/scala/src/gs/std/v0/effect/pipes/SHA256PipeTests.scala +++ /dev/null @@ -1,91 +0,0 @@ -package gs.std.v0.effect.pipes - -import cats.effect.IO -import cats.effect.unsafe.IORuntime -import fs2.Stream -import gs.std.v0.core.SHA256 -import java.security.MessageDigest -import munit.* - -class SHA256PipeTests extends FunSuite: - - import SHA256PipeTests.Data - - given runtime: IORuntime = IORuntime.global - - def io( - name: String - )( - body: => IO[Unit] - )( - using - loc: Location - ): Unit = - test(name)(body.unsafeRunSync())( - using loc - ) - - def getDigest(): MessageDigest = MessageDigest.getInstance(SHA256.Algorithm) - - def shaDefault(data: String): SHA256 = - SHA256.calculate(data) - - def shaStream(data: String): IO[SHA256] = - Stream - .evals(IO(data.getBytes().toList)) - .through(SHA256Pipe.apply[IO]) - .compile - .toList - .map { - case sha :: Nil => sha - case Nil => fail("Received an empty SHA-256.") - case _ => fail("Received multiple SHA-256.") - } - - def readResource(resourceName: String): Stream[IO, Byte] = - fs2.io.readInputStream( - fis = IO(getClass().getClassLoader().getResourceAsStream(resourceName)), - chunkSize = 1024, - closeAfterUse = true - ) - - io("should calculate the SHA-256 for some fixed (pre-calculated) value") { - val data = "some-fixed-value" - val expectedHex = - "d5dae4f21bc27a568842bc9b467e69bd823d54d1c9e89d5ea662493068d5100d" - val basic = shaDefault(data) - - shaStream(data).map { streamed => - assertEquals(basic.hex().data, expectedHex) - assertEquals(streamed.hex().data, expectedHex) - assert(streamed.isSame(basic)) - } - } - - io("should calculate the SHA-256 for a 1M file") { - readResource(Data.File1Path) - .through(SHA256Pipe[IO]) - .compile - .toList - .map { - case sha :: Nil => sha - case Nil => fail("Received an empty SHA-256.") - case _ => fail("Received multiple SHA-256.") - } - .map(sha => assertEquals(sha.hex().data, Data.File1Sha)) - } - -end SHA256PipeTests - -object SHA256PipeTests: - - object Data: - - val File1Path: String = "sha256-input" - - val File1Sha: String = - "42d96cff2300306954c166ab75862d4e5fd7ce977bb34f7e8ee785f62244d7a0" - - end Data - -end SHA256PipeTests diff --git a/modules/io/src/main/scala/src/gs/std/v0/io/Files.scala b/modules/io/src/main/scala/src/gs/std/v0/io/Files.scala index 43e5e02..8b7c6bd 100644 --- a/modules/io/src/main/scala/src/gs/std/v0/io/Files.scala +++ b/modules/io/src/main/scala/src/gs/std/v0/io/Files.scala @@ -11,10 +11,11 @@ import java.nio.file.Path import java.nio.file.StandardOpenOption import scala.jdk.CollectionConverters.* -/** Common operations on files, wrapped using Cats Effect. If you want to Stream - * files, the `fs2-io` package is the place to go. +/** Common operations on files. * * Includes wrappers around some `java.nio.file.Files` functionality. + * + * For streaming operations, just use the `fs2.io` library. */ object Files: