fs2 already has it
Some checks failed
/ Build and Release Library (push) Has been cancelled

This commit is contained in:
Pat Garrity 2026-05-06 21:51:33 -05:00
parent 4a10958731
commit 146afe6a88
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
4 changed files with 57 additions and 170 deletions

View file

@ -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[_]]`)

View file

@ -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

View file

@ -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

View file

@ -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: