This commit is contained in:
parent
4a10958731
commit
146afe6a88
4 changed files with 57 additions and 170 deletions
85
README.md
85
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[_]]`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue