(patch) first useful build of the library
Some checks failed
/ Build and Release Library (push) Failing after 2m4s
Some checks failed
/ Build and Release Library (push) Failing after 2m4s
This commit is contained in:
parent
ea82a5ff44
commit
1f43c9815e
32 changed files with 1431 additions and 699 deletions
80
README.md
80
README.md
|
|
@ -3,52 +3,70 @@
|
||||||
[GS Open Source](https://garrity.co/oss.html) |
|
[GS Open Source](https://garrity.co/oss.html) |
|
||||||
[License (Apache 2.0)](./LICENSE)
|
[License (Apache 2.0)](./LICENSE)
|
||||||
|
|
||||||
Garrity Software standard types and operations. Provides a zero-dependency
|
Garrity Software standard types and operations with minimal dependencies.
|
||||||
collection of basic tools.
|
|
||||||
|
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Dependency](#dependency)
|
- [Core Module](#core-module)
|
||||||
- [Donate](#donate)
|
- [I/O Module](#io-module)
|
||||||
|
- [Effect Module](#effect-module)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Dependency
|
|
||||||
|
|
||||||
This artifact is available in the Garrity Software Maven repository.
|
This artifact is available in the Garrity Software Maven repository.
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
externalResolvers +=
|
externalResolvers +=
|
||||||
"Garrity Software Releases" at "https://maven.garrity.co/releases"
|
"Garrity Software Releases" at "https://maven.garrity.co/releases"
|
||||||
|
|
||||||
val GsStd: ModuleID =
|
// No Dependencies
|
||||||
"gs" %% "gs-std-v0" % "$VERSION"
|
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`
|
Provides standard types, wrappers, and tools.
|
||||||
- `Size`
|
|
||||||
- `ByteCount`
|
|
||||||
- `Blob`
|
|
||||||
- `CreatedAt`
|
|
||||||
- `UpdatedAt`
|
|
||||||
- `SHA256`
|
|
||||||
- `MD5`
|
|
||||||
- `EncodedString`
|
|
||||||
- `B64`
|
|
||||||
- `B64Url`
|
|
||||||
- `Hex`
|
|
||||||
|
|
||||||
## Tools
|
- Common semantic types (e.g. `CreatedAt`).
|
||||||
|
- Encoding and representing encoded data.
|
||||||
|
- Hashing algorithms and representing hashes.
|
||||||
|
- Reasoning about bytes and blobs.
|
||||||
|
|
||||||
- `Encoder`
|
## I/O Module
|
||||||
- `Base64Encoder`
|
|
||||||
- `HexEncoder`
|
|
||||||
- `Decoder`
|
|
||||||
- `Base64Decoder`
|
|
||||||
- `HexDecoder`
|
|
||||||
|
|
||||||
## Donate
|
### File Helpers
|
||||||
|
|
||||||
Enjoy this project or want to help me achieve my [goals](https://garrity.co)?
|
`Files`: These provide support for reading and writing files. Provides wrapping
|
||||||
Consider [Donating to Pat on Ko-fi](https://ko-fi.com/gspfm).
|
around `java.nio.file.Files`.
|
||||||
|
|
||||||
|
Note that for streaming operations, `fs2.io` provides everything conveniently
|
||||||
|
and does not benefit from wrapping.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
|
||||||
53
build.sbt
53
build.sbt
|
|
@ -10,6 +10,10 @@ ThisBuild / licenses := Seq(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val noPublishSettings = Seq(
|
||||||
|
publish := {}
|
||||||
|
)
|
||||||
|
|
||||||
val sharedSettings = Seq(
|
val sharedSettings = Seq(
|
||||||
scalaVersion := scala3,
|
scalaVersion := scala3,
|
||||||
version := semVerSelected.value,
|
version := semVerSelected.value,
|
||||||
|
|
@ -18,14 +22,61 @@ val sharedSettings = Seq(
|
||||||
coverageMinimumBranchTotal := 100
|
coverageMinimumBranchTotal := 100
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val Deps = new {
|
||||||
|
|
||||||
|
val Cats = new {
|
||||||
|
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
|
||||||
|
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.7.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
val Fs2 = new {
|
||||||
|
val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.13.0"
|
||||||
|
val IO: ModuleID = "co.fs2" %% "fs2-io" % "3.13.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.3.0" % Test
|
||||||
|
}
|
||||||
|
|
||||||
lazy val testSettings = Seq(
|
lazy val testSettings = Seq(
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.scalameta" %% "munit" % "1.3.0" % Test
|
Deps.MUnit
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val `gs-std` = project
|
lazy val `gs-std` = project
|
||||||
.in(file("."))
|
.in(file("."))
|
||||||
|
.aggregate(core, io, effect)
|
||||||
.settings(sharedSettings)
|
.settings(sharedSettings)
|
||||||
.settings(testSettings)
|
.settings(testSettings)
|
||||||
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
||||||
|
|
||||||
|
lazy val core = project
|
||||||
|
.in(file("modules/core"))
|
||||||
|
.settings(sharedSettings)
|
||||||
|
.settings(testSettings)
|
||||||
|
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
|
||||||
|
|
||||||
|
lazy val io = project
|
||||||
|
.in(file("modules/io"))
|
||||||
|
.settings(sharedSettings)
|
||||||
|
.settings(testSettings)
|
||||||
|
.settings(name := s"${gsProjectName.value}-io-v${semVerMajor.value}")
|
||||||
|
.settings(
|
||||||
|
libraryDependencies ++= Seq(
|
||||||
|
Deps.Cats.Core,
|
||||||
|
Deps.Cats.Effect,
|
||||||
|
Deps.Fs2.Core,
|
||||||
|
Deps.Fs2.IO
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val effect = project
|
||||||
|
.in(file("modules/effect"))
|
||||||
|
.dependsOn(core)
|
||||||
|
.dependsOn(io % "test->compile")
|
||||||
|
.settings(sharedSettings)
|
||||||
|
.settings(testSettings)
|
||||||
|
.settings(name := s"${gsProjectName.value}-effect-v${semVerMajor.value}")
|
||||||
|
.settings(
|
||||||
|
libraryDependencies ++= Seq(Deps.Cats.Core, Deps.Cats.Effect, Deps.Fs2.Core)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
import java.{util => ju}
|
import java.{util => ju}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
49
modules/core/src/main/scala/src/gs/std/v0/core/Blob.scala
Normal file
49
modules/core/src/main/scala/src/gs/std/v0/core/Blob.scala
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
|
/** Represents a blob -- some array of bytes.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* The underlying data.
|
||||||
|
*/
|
||||||
|
final class Blob(private val data: Array[Byte]) extends IndexedSeq[Byte]:
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def apply(i: Int): Byte = byteAt(i)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def length: Int = data.length
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The number of bytes in this blob, expressed as a count.
|
||||||
|
*/
|
||||||
|
def numberOfBytes: ByteCount = ByteCount(data.length)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def equals(obj: Any): Boolean =
|
||||||
|
obj match
|
||||||
|
case other: Blob => data.sameElements(other.data)
|
||||||
|
case other: Array[Byte] => data.sameElements(other)
|
||||||
|
case _ => false
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def hashCode(): Int = data.hashCode()
|
||||||
|
|
||||||
|
/** Retrieve the byte at the given index.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index.
|
||||||
|
* @return
|
||||||
|
* The byte stored at the given index.
|
||||||
|
*/
|
||||||
|
def byteAt(index: Int): Byte = data.apply(index)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* This byte array, encoded as a base64 string.
|
||||||
|
*/
|
||||||
|
def base64(): String = Base64.getEncoder().encodeToString(data)
|
||||||
138
modules/core/src/main/scala/src/gs/std/v0/core/ByteCount.scala
Normal file
138
modules/core/src/main/scala/src/gs/std/v0/core/ByteCount.scala
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
/** Opaque type for some number of bytes (>= 0).
|
||||||
|
*/
|
||||||
|
opaque type ByteCount = Long
|
||||||
|
|
||||||
|
/** Opaque type for some number of bytes (>= 0).
|
||||||
|
*/
|
||||||
|
object ByteCount:
|
||||||
|
|
||||||
|
/** 0 bytes.
|
||||||
|
*/
|
||||||
|
final val Zero: ByteCount = 0
|
||||||
|
|
||||||
|
/** 1,000 bytes.
|
||||||
|
*/
|
||||||
|
final val OneKilobyte: ByteCount = 1000
|
||||||
|
|
||||||
|
/** 1,000,000 bytes.
|
||||||
|
*/
|
||||||
|
final val OneMegabyte: ByteCount = 1000000
|
||||||
|
|
||||||
|
/** 1,000,000,000 bytes.
|
||||||
|
*/
|
||||||
|
final val OneGigabyte: ByteCount = 1000000000
|
||||||
|
|
||||||
|
/** Express the given number as a byte count. All values are normalized to the
|
||||||
|
* absolute value -- negative values are coerced to positive.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The input integer.
|
||||||
|
* @return
|
||||||
|
* The [[ByteCount]] instance.
|
||||||
|
*/
|
||||||
|
def apply(value: Long): ByteCount = Math.abs(value)
|
||||||
|
|
||||||
|
/** 1 kilobyte = 1,000 bytes
|
||||||
|
*
|
||||||
|
* @param kb
|
||||||
|
* The number of kilobytes.
|
||||||
|
* @return
|
||||||
|
* The number of bytes.
|
||||||
|
*/
|
||||||
|
def fromKilobytes(kb: Long): ByteCount =
|
||||||
|
Math.abs(kb) * 1000L
|
||||||
|
|
||||||
|
/** 1 megabyte = 1,000,000 bytes
|
||||||
|
*
|
||||||
|
* @param mb
|
||||||
|
* The number of megabytes.
|
||||||
|
* @return
|
||||||
|
* The number of bytes.
|
||||||
|
*/
|
||||||
|
def fromMegabytes(mb: Long): ByteCount =
|
||||||
|
Math.abs(mb) * 1000000L
|
||||||
|
|
||||||
|
/** 1 gigabyte = 1,000,000,000 bytes
|
||||||
|
*
|
||||||
|
* @param mb
|
||||||
|
* The number of gigabytes.
|
||||||
|
* @return
|
||||||
|
* The number of bytes.
|
||||||
|
*/
|
||||||
|
def fromGigabytes(gb: Long): ByteCount =
|
||||||
|
Math.abs(gb) * 1000000000
|
||||||
|
|
||||||
|
given CanEqual[ByteCount, ByteCount] = CanEqual.derived
|
||||||
|
|
||||||
|
given Ordering[ByteCount] with
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
def compare(
|
||||||
|
x: ByteCount,
|
||||||
|
y: ByteCount
|
||||||
|
): Int =
|
||||||
|
if x > y then 1 else if x == y then 0 else -1
|
||||||
|
|
||||||
|
extension (byteCount: ByteCount)
|
||||||
|
/** @return
|
||||||
|
* The underlying `Long`.
|
||||||
|
*/
|
||||||
|
def unwrap(): Long = byteCount
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The underlying `Long`.
|
||||||
|
*/
|
||||||
|
def toLong(): Long = byteCount
|
||||||
|
|
||||||
|
/** 1 kilobyte = 1,000 bytes
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The number of kilobytes represented by this count.
|
||||||
|
*/
|
||||||
|
def toKilobytes(): Double = byteCount / 1000.0
|
||||||
|
|
||||||
|
/** 1 megabyte = 1,000,000 bytes.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The number of megabytes represented by this count.
|
||||||
|
*/
|
||||||
|
def toMegabytes(): Double = byteCount / 1000000.0
|
||||||
|
|
||||||
|
/** 1 gigabyte = 1,000,000,000 bytes.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The number of gigabytes represented by this count.
|
||||||
|
*/
|
||||||
|
def toGigabytes(): Double = byteCount / 1000000000.0
|
||||||
|
|
||||||
|
/** Add some count to this one.
|
||||||
|
*
|
||||||
|
* @param that
|
||||||
|
* The number to add.
|
||||||
|
* @return
|
||||||
|
* The sum of the numbers.
|
||||||
|
*/
|
||||||
|
def +(that: ByteCount): ByteCount = byteCount + that
|
||||||
|
|
||||||
|
/** Multiply this count by some other count.
|
||||||
|
*
|
||||||
|
* @param that
|
||||||
|
* The number to multiply by.
|
||||||
|
* @return
|
||||||
|
* The product of the numbers.
|
||||||
|
*/
|
||||||
|
def *(that: ByteCount): ByteCount = byteCount * that
|
||||||
|
|
||||||
|
/** Check if this value is the same as some number.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The number to compare against.
|
||||||
|
* @return
|
||||||
|
* True if the values are equal, false otherwise.
|
||||||
|
*/
|
||||||
|
def equal(value: Int): Boolean = byteCount == value.toLong
|
||||||
|
|
||||||
|
end ByteCount
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
/** Opaque type that represents the instant something was created.
|
||||||
|
*/
|
||||||
|
opaque type CreatedAt = Instant
|
||||||
|
|
||||||
|
/** Opaque type that represents the instant something was created.
|
||||||
|
*/
|
||||||
|
object CreatedAt:
|
||||||
|
|
||||||
|
/** Instantiate a new [[CreatedAt]] from the given `java.time.Instant`.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The value to semantically represent.
|
||||||
|
* @return
|
||||||
|
* The new [[CreatedAt]].
|
||||||
|
*/
|
||||||
|
def apply(value: Instant): CreatedAt = value
|
||||||
|
|
||||||
|
given CanEqual[CreatedAt, CreatedAt] = CanEqual.derived
|
||||||
|
|
||||||
|
given Ordering[CreatedAt] = Ordering[Instant]
|
||||||
|
|
||||||
|
extension (createdAt: Instant)
|
||||||
|
/** @return
|
||||||
|
* The underlying `java.time.Instant`.
|
||||||
|
*/
|
||||||
|
def unwrap(): Instant = createdAt
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The underlying `java.time.Instant`.
|
||||||
|
*/
|
||||||
|
def toInstant(): Instant = createdAt
|
||||||
|
|
||||||
|
end CreatedAt
|
||||||
13
modules/core/src/main/scala/src/gs/std/v0/core/Decoder.scala
Normal file
13
modules/core/src/main/scala/src/gs/std/v0/core/Decoder.scala
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
/** Interface for byte decoding from encoded String formats.
|
||||||
|
*/
|
||||||
|
trait Decoder[-A <: EncodedString]:
|
||||||
|
/** Decode an input string to an array of bytes.
|
||||||
|
*
|
||||||
|
* @param input
|
||||||
|
* The input to decode.
|
||||||
|
* @return
|
||||||
|
* The decoded byte array.
|
||||||
|
*/
|
||||||
|
def decode(input: A): Array[Byte]
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
/** Represents string-encoded data.
|
/** Represents string-encoded data.
|
||||||
*
|
*
|
||||||
|
|
@ -52,7 +52,7 @@ final class B64(
|
||||||
object B64:
|
object B64:
|
||||||
|
|
||||||
/** Instantiate [[B64]] from the given string. Assumes that the input is
|
/** Instantiate [[B64]] from the given string. Assumes that the input is
|
||||||
* base64-encoded.
|
* base64-encoded.
|
||||||
*
|
*
|
||||||
* This function does NOT validate the input.
|
* This function does NOT validate the input.
|
||||||
*
|
*
|
||||||
|
|
@ -98,7 +98,7 @@ final class B64Url(
|
||||||
object B64Url:
|
object B64Url:
|
||||||
|
|
||||||
/** Instantiate [[B64Url]] from the given string. Assumes that the input is
|
/** Instantiate [[B64Url]] from the given string. Assumes that the input is
|
||||||
* base64-encoded.
|
* base64-encoded.
|
||||||
*
|
*
|
||||||
* This function does NOT validate the input.
|
* This function does NOT validate the input.
|
||||||
*
|
*
|
||||||
|
|
@ -144,7 +144,7 @@ final class Hex(
|
||||||
object Hex:
|
object Hex:
|
||||||
|
|
||||||
/** Instantiate [[Hex]] from the given string. Assumes that the input is
|
/** Instantiate [[Hex]] from the given string. Assumes that the input is
|
||||||
* hex-encoded.
|
* hex-encoded.
|
||||||
*
|
*
|
||||||
* This function does NOT validate the input.
|
* This function does NOT validate the input.
|
||||||
*
|
*
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
import java.util.HexFormat
|
import java.util.HexFormat
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.std.v0
|
package gs.std.v0.core
|
||||||
|
|
||||||
import java.util.HexFormat
|
import java.util.HexFormat
|
||||||
|
|
||||||
108
modules/core/src/main/scala/src/gs/std/v0/core/MD5.scala
Normal file
108
modules/core/src/main/scala/src/gs/std/v0/core/MD5.scala
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/** Opaque type representing a MD5 hash.
|
||||||
|
*/
|
||||||
|
opaque type MD5 = Array[Byte]
|
||||||
|
|
||||||
|
/** Opaque type representing a MD5 hash.
|
||||||
|
*/
|
||||||
|
object MD5:
|
||||||
|
|
||||||
|
/** MD5 hashes are exactly 16 bytes.
|
||||||
|
*/
|
||||||
|
final val NumberOfBytes: ByteCount = ByteCount(16)
|
||||||
|
|
||||||
|
/** The algorithm name is "MD5".
|
||||||
|
*/
|
||||||
|
final val Algorithm: String = "MD5"
|
||||||
|
|
||||||
|
/** Instantiate a [[MD5]] from the given byte array. This function does not
|
||||||
|
* know whether the array is actually a calculated hash.
|
||||||
|
*
|
||||||
|
* Typically used for loading pre-validated hashes (e.g. from a database).
|
||||||
|
*
|
||||||
|
* @param bytes
|
||||||
|
* The bytes - must contain exactly 16 bytes.
|
||||||
|
* @return
|
||||||
|
* The new [[MD5]] instance.
|
||||||
|
*/
|
||||||
|
def fromBytes(bytes: Array[Byte]): MD5 =
|
||||||
|
if NumberOfBytes.equal(bytes.length) then bytes
|
||||||
|
else
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
s"MD5 values must be exactly $NumberOfBytes bytes."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Calculate the MD5 hash for the given byte array.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* The byte array.
|
||||||
|
* @return
|
||||||
|
* The calculated [[MD5]].
|
||||||
|
*/
|
||||||
|
def calculate(data: Array[Byte]): MD5 =
|
||||||
|
MessageDigest.getInstance(Algorithm).digest(data)
|
||||||
|
|
||||||
|
/* Calculate the MD5 hash for the given string.
|
||||||
|
*
|
||||||
|
* @param data The string data.
|
||||||
|
* @param charset The character set of the string. Defaults to UTF-8.
|
||||||
|
* @return The calculated [[MD5]]. */
|
||||||
|
def calculate(
|
||||||
|
data: String,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): MD5 =
|
||||||
|
calculate(data.getBytes(charset))
|
||||||
|
|
||||||
|
extension (md5: MD5)
|
||||||
|
/** @return
|
||||||
|
* The underlying byte array.
|
||||||
|
*/
|
||||||
|
def unwrap(): Array[Byte] = md5
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The underlying byte array.
|
||||||
|
*/
|
||||||
|
def toBytes(): Array[Byte] = md5
|
||||||
|
|
||||||
|
/** Get the byte at the given index (0 to 15).
|
||||||
|
*
|
||||||
|
* Throws an exception if an out-of-bound index is given.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The byte index (0 to 15).
|
||||||
|
* @return
|
||||||
|
* The byte at the specified index.
|
||||||
|
*/
|
||||||
|
def getByte(index: Int): Byte =
|
||||||
|
if index < 0 || index >= NumberOfBytes.unwrap() then
|
||||||
|
throw IndexOutOfBoundsException(
|
||||||
|
s"Index $index out of MD5 bound of $NumberOfBytes bytes."
|
||||||
|
)
|
||||||
|
else md5.apply(index)
|
||||||
|
|
||||||
|
/** Determine if this hash is the same as some other hash. Compares each
|
||||||
|
* byte in order.
|
||||||
|
*
|
||||||
|
* @param other
|
||||||
|
* The [[MD5]] to compare against.
|
||||||
|
* @return
|
||||||
|
* True if the hashes are identical, false otherwise.
|
||||||
|
*/
|
||||||
|
def isSame(other: MD5): Boolean = md5.sameElements(other)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* This hash encoded to a Base64 string.
|
||||||
|
*/
|
||||||
|
def base64(): EncodedString = Base64Encoder.encode(md5)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* This hash encoded to a Hexadecimal string.
|
||||||
|
*/
|
||||||
|
def hex(): EncodedString = HexEncoder.encode(md5)
|
||||||
|
|
||||||
|
end MD5
|
||||||
84
modules/core/src/main/scala/src/gs/std/v0/core/Nat.scala
Normal file
84
modules/core/src/main/scala/src/gs/std/v0/core/Nat.scala
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
/** Opaque type for the natural numbers (including 0).
|
||||||
|
*/
|
||||||
|
opaque type Nat = Int
|
||||||
|
|
||||||
|
/** Opaque type for the natural numbers (including 0).
|
||||||
|
*/
|
||||||
|
object Nat:
|
||||||
|
|
||||||
|
sealed trait Invalid
|
||||||
|
object Invalid extends Invalid
|
||||||
|
|
||||||
|
/** The number 0.
|
||||||
|
*/
|
||||||
|
final val Zero: Nat = 0
|
||||||
|
|
||||||
|
/** The number 1.
|
||||||
|
*/
|
||||||
|
final val One: Nat = 1
|
||||||
|
|
||||||
|
/** Express the given integer as a natural number.
|
||||||
|
*
|
||||||
|
* Throws an `IllegalArgumentException` if a negative value is given as
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The input integer.
|
||||||
|
* @return
|
||||||
|
* The [[Nat]] instance.
|
||||||
|
*/
|
||||||
|
def apply(value: Int): Nat =
|
||||||
|
if value >= 0 then value
|
||||||
|
else throw new IllegalArgumentException("Nat values must be 0 or greater.")
|
||||||
|
|
||||||
|
def validate(value: Int): Either[Invalid, Nat] =
|
||||||
|
if value >= 0 then Right(value) else Left(Invalid)
|
||||||
|
|
||||||
|
given CanEqual[Nat, Nat] = CanEqual.derived
|
||||||
|
|
||||||
|
given Ordering[Nat] with
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
def compare(
|
||||||
|
x: Nat,
|
||||||
|
y: Nat
|
||||||
|
): Int = x - y
|
||||||
|
|
||||||
|
extension (nat: Nat)
|
||||||
|
/** @return
|
||||||
|
* The underlying integer.
|
||||||
|
*/
|
||||||
|
def unwrap(): Int = nat
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next integer.
|
||||||
|
*/
|
||||||
|
def next(): Nat = nat + 1
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next integer.
|
||||||
|
*/
|
||||||
|
def increment(): Nat = nat + 1
|
||||||
|
|
||||||
|
/** Add some natural number to this one.
|
||||||
|
*
|
||||||
|
* @param that
|
||||||
|
* The number to add.
|
||||||
|
* @return
|
||||||
|
* The sum of the numbers.
|
||||||
|
*/
|
||||||
|
def +(that: Nat): Nat = nat + that
|
||||||
|
|
||||||
|
/** Multiply this natural number by some other natural number.
|
||||||
|
*
|
||||||
|
* @param that
|
||||||
|
* The number to multiply by.
|
||||||
|
* @return
|
||||||
|
* The product of the numbers.
|
||||||
|
*/
|
||||||
|
def *(that: Nat): Nat = nat * that
|
||||||
|
|
||||||
|
end Nat
|
||||||
112
modules/core/src/main/scala/src/gs/std/v0/core/SHA256.scala
Normal file
112
modules/core/src/main/scala/src/gs/std/v0/core/SHA256.scala
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/** Opaque type representing a SHA-256 hash.
|
||||||
|
*/
|
||||||
|
opaque type SHA256 = Array[Byte]
|
||||||
|
|
||||||
|
/** Opaque type representing a SHA-256 hash.
|
||||||
|
*/
|
||||||
|
object SHA256:
|
||||||
|
|
||||||
|
/** SHA-256 hashes are exactly 32 bytes.
|
||||||
|
*/
|
||||||
|
final val NumberOfBytes: ByteCount = ByteCount(32)
|
||||||
|
|
||||||
|
/** The algorithm name is "SHA-256".
|
||||||
|
*/
|
||||||
|
final val Algorithm: String = "SHA-256"
|
||||||
|
|
||||||
|
/** Instantiate a [[SHA256]] from the given byte array. This function does not
|
||||||
|
* know whether the array is actually a calculated hash.
|
||||||
|
*
|
||||||
|
* Typically used for loading pre-validated hashes (e.g. from a database).
|
||||||
|
*
|
||||||
|
* @param bytes
|
||||||
|
* The bytes - must contain exactly 32 bytes.
|
||||||
|
* @return
|
||||||
|
* The new [[SHA256]] instance.
|
||||||
|
*/
|
||||||
|
def fromBytes(bytes: Array[Byte]): SHA256 =
|
||||||
|
if NumberOfBytes.equal(bytes.length) then bytes
|
||||||
|
else
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
s"SHA-256 values must be exactly $NumberOfBytes bytes."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Calculate the SHA-256 hash for the given byte array.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* The byte array.
|
||||||
|
* @return
|
||||||
|
* The calculated [[SHA256]].
|
||||||
|
*/
|
||||||
|
def calculate(data: Array[Byte]): SHA256 =
|
||||||
|
MessageDigest.getInstance(Algorithm).digest(data)
|
||||||
|
|
||||||
|
/** Calculate the SHA-256 hash for the given string.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* The string data.
|
||||||
|
* @param charset
|
||||||
|
* The character set of the string. Defaults to UTF-8.
|
||||||
|
* @return
|
||||||
|
* The calculated [[SHA256]].
|
||||||
|
*/
|
||||||
|
def calculate(
|
||||||
|
data: String,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): SHA256 =
|
||||||
|
calculate(data.getBytes(charset))
|
||||||
|
|
||||||
|
extension (sha: SHA256)
|
||||||
|
/** @return
|
||||||
|
* The underlying byte array.
|
||||||
|
*/
|
||||||
|
def unwrap(): Array[Byte] = sha
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The underlying byte array.
|
||||||
|
*/
|
||||||
|
def toBytes(): Array[Byte] = sha
|
||||||
|
|
||||||
|
/** Get the byte at the given index (0 to 31).
|
||||||
|
*
|
||||||
|
* Throws an exception if an out-of-bound index is given.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The byte index (0 to 31).
|
||||||
|
* @return
|
||||||
|
* The byte at the specified index.
|
||||||
|
*/
|
||||||
|
def getByte(index: Int): Byte =
|
||||||
|
if index < 0 || index >= NumberOfBytes.unwrap() then
|
||||||
|
throw IndexOutOfBoundsException(
|
||||||
|
s"Index $index out of SHA-256 bound of $NumberOfBytes bytes."
|
||||||
|
)
|
||||||
|
else sha.apply(index)
|
||||||
|
|
||||||
|
/** Determine if this hash is the same as some other hash. Compares each
|
||||||
|
* byte in order.
|
||||||
|
*
|
||||||
|
* @param other
|
||||||
|
* The [[SHA256]] to compare against.
|
||||||
|
* @return
|
||||||
|
* True if the hashes are identical, false otherwise.
|
||||||
|
*/
|
||||||
|
def isSame(other: SHA256): Boolean = sha.sameElements(other)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* This hash encoded to a Base64 string.
|
||||||
|
*/
|
||||||
|
def base64(): EncodedString = Base64Encoder.encode(sha)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* This hash encoded to a Hexadecimal string.
|
||||||
|
*/
|
||||||
|
def hex(): EncodedString = HexEncoder.encode(sha)
|
||||||
|
|
||||||
|
end SHA256
|
||||||
102
modules/core/src/main/scala/src/gs/std/v0/core/Size.scala
Normal file
102
modules/core/src/main/scala/src/gs/std/v0/core/Size.scala
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
/** Opaque type for collection sizes. Values are guaranteed to be 0 or greater.
|
||||||
|
*/
|
||||||
|
opaque type Size = Int
|
||||||
|
|
||||||
|
/** Opaque type for collection sizes. Values are guaranteed to be 0 or greater.
|
||||||
|
*/
|
||||||
|
object Size:
|
||||||
|
|
||||||
|
sealed trait Invalid
|
||||||
|
object Invalid extends Invalid
|
||||||
|
|
||||||
|
/** The size 0.
|
||||||
|
*/
|
||||||
|
final val Zero: Size = 0
|
||||||
|
|
||||||
|
/** The size 1.
|
||||||
|
*/
|
||||||
|
final val One: Size = 1
|
||||||
|
|
||||||
|
/** Express the given integer as a size.
|
||||||
|
*
|
||||||
|
* Throws an `IllegalArgumentException` if a negative value is given as
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The input integer.
|
||||||
|
* @return
|
||||||
|
* The [[Size]] instance.
|
||||||
|
*/
|
||||||
|
def apply(value: Int): Size =
|
||||||
|
if value >= 0 then value
|
||||||
|
else throw new IllegalArgumentException("Size values must be 0 or greater.")
|
||||||
|
|
||||||
|
def validate(value: Int): Either[Invalid, Size] =
|
||||||
|
if value >= 0 then Right(value) else Left(Invalid)
|
||||||
|
|
||||||
|
/** Express the size of any collection.
|
||||||
|
*
|
||||||
|
* @param iter
|
||||||
|
* The collection.
|
||||||
|
* @return
|
||||||
|
* The size of the collection.
|
||||||
|
*/
|
||||||
|
def of(iter: Iterable[?]): Size = iter.size
|
||||||
|
|
||||||
|
/** Express the size of any array.
|
||||||
|
*
|
||||||
|
* @param arr
|
||||||
|
* The array.
|
||||||
|
* @return
|
||||||
|
* The size of the array.
|
||||||
|
*/
|
||||||
|
def of(arr: Array[?]): Size = arr.length
|
||||||
|
|
||||||
|
given CanEqual[Size, Size] = CanEqual.derived
|
||||||
|
|
||||||
|
given Ordering[Size] with
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
def compare(
|
||||||
|
x: Size,
|
||||||
|
y: Size
|
||||||
|
): Int = x - y
|
||||||
|
|
||||||
|
extension (size: Size)
|
||||||
|
/** @return
|
||||||
|
* The underlying integer.
|
||||||
|
*/
|
||||||
|
def unwrap(): Int = size
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next value.
|
||||||
|
*/
|
||||||
|
def next(): Size = size + 1
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next value.
|
||||||
|
*/
|
||||||
|
def increment(): Size = size + 1
|
||||||
|
|
||||||
|
/** Add some size to this one.
|
||||||
|
*
|
||||||
|
* @param that
|
||||||
|
* The number to add.
|
||||||
|
* @return
|
||||||
|
* The sum of the numbers.
|
||||||
|
*/
|
||||||
|
def +(that: Size): Size = size + that
|
||||||
|
|
||||||
|
/** Multiply this size by some other size.
|
||||||
|
*
|
||||||
|
* @param that
|
||||||
|
* The number to multiply by.
|
||||||
|
* @return
|
||||||
|
* The product of the numbers.
|
||||||
|
*/
|
||||||
|
def *(that: Size): Size = size * that
|
||||||
|
|
||||||
|
end Size
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package gs.std.v0.core
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
/** Opaque type that represents the instant something was updated.
|
||||||
|
*/
|
||||||
|
opaque type UpdatedAt = Instant
|
||||||
|
|
||||||
|
/** Opaque type that represents the instant something was updated.
|
||||||
|
*/
|
||||||
|
object UpdatedAt:
|
||||||
|
|
||||||
|
/** Instantiate a new [[UpdatedAt]] from the given `java.time.Instant`.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The value to semantically represent.
|
||||||
|
* @return
|
||||||
|
* The new [[UpdatedAt]].
|
||||||
|
*/
|
||||||
|
def apply(value: Instant): UpdatedAt = value
|
||||||
|
|
||||||
|
given CanEqual[UpdatedAt, UpdatedAt] = CanEqual.derived
|
||||||
|
|
||||||
|
given Ordering[UpdatedAt] = Ordering[Instant]
|
||||||
|
|
||||||
|
extension (updatedAt: Instant)
|
||||||
|
/** @return
|
||||||
|
* The underlying `java.time.Instant`.
|
||||||
|
*/
|
||||||
|
def unwrap(): Instant = updatedAt
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The underlying `java.time.Instant`.
|
||||||
|
*/
|
||||||
|
def toInstant(): Instant = updatedAt
|
||||||
|
|
||||||
|
end UpdatedAt
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package gs.std.v0.effect
|
||||||
|
|
||||||
|
import cats.effect.Sync
|
||||||
|
import cats.syntax.all.*
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
/** Implementation of [[DateTimeProvider]] based on a specific
|
||||||
|
* `java.time.Clock`. All dates and times are calculated using this clock.
|
||||||
|
*
|
||||||
|
* @param clock
|
||||||
|
* The clock used to calculate dates and times.
|
||||||
|
*/
|
||||||
|
final class ClockDateTimeProvider[F[_]: Sync](
|
||||||
|
val clock: Clock
|
||||||
|
) extends DateTimeProvider[F]:
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def timestamp(): F[Instant] =
|
||||||
|
Sync[F].delay(Instant.now(clock))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nowInstant(): F[Instant] =
|
||||||
|
Sync[F].delay(Instant.now(clock))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nowZoned(): F[ZonedDateTime] =
|
||||||
|
Sync[F].delay(ZonedDateTime.now(clock))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nowOffset(): F[OffsetDateTime] =
|
||||||
|
Sync[F].delay(OffsetDateTime.now(clock))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def today(): F[LocalDate] =
|
||||||
|
Sync[F].delay(LocalDate.now(clock))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def yesterday(): F[LocalDate] =
|
||||||
|
today().map(_.minusDays(1L))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def tomorrow(): F[LocalDate] =
|
||||||
|
today().map(_.plusDays(1L))
|
||||||
|
|
||||||
|
object ClockDateTimeProvider:
|
||||||
|
|
||||||
|
/** Instantiate a new [[ClockDateTimeProvider]].
|
||||||
|
*
|
||||||
|
* @param clock
|
||||||
|
* The clock to use for all date/time calculations.
|
||||||
|
* @return
|
||||||
|
* The new [[DateTimeProvider]] instance.
|
||||||
|
*/
|
||||||
|
def apply[F[_]: Sync](clock: Clock): DateTimeProvider[F] =
|
||||||
|
new ClockDateTimeProvider[F](clock)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* A new [[ClockDateTimeProvider]] based on the system's default zone using
|
||||||
|
* the system's clock.
|
||||||
|
*/
|
||||||
|
def system[F[_]: Sync]: DateTimeProvider[F] =
|
||||||
|
new ClockDateTimeProvider[F](Clock.systemDefaultZone())
|
||||||
|
|
||||||
|
/** Use a system clock aligned to the given zone.
|
||||||
|
*
|
||||||
|
* @param zoneId
|
||||||
|
* The identifier of the zone to target.
|
||||||
|
* @return
|
||||||
|
* A new [[ClockDateTimeProvider]] based on the specified zone using the
|
||||||
|
* system's clock.
|
||||||
|
*/
|
||||||
|
def forZone[F[_]: Sync](zoneId: ZoneId): DateTimeProvider[F] =
|
||||||
|
new ClockDateTimeProvider[F](Clock.system(zoneId))
|
||||||
|
|
||||||
|
end ClockDateTimeProvider
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package gs.std.v0.effect
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/** Provides date and time values.
|
||||||
|
*/
|
||||||
|
trait DateTimeProvider[F[_]]:
|
||||||
|
/** @return
|
||||||
|
* The current timestamp (`java.time.Instant`).
|
||||||
|
*/
|
||||||
|
def timestamp(): F[Instant]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The current `java.time.Instant`.
|
||||||
|
*/
|
||||||
|
def nowInstant(): F[Instant]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The current date and time in the configured zone.
|
||||||
|
*/
|
||||||
|
def nowZoned(): F[ZonedDateTime]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The current date and time in the configured offset.
|
||||||
|
*/
|
||||||
|
def nowOffset(): F[OffsetDateTime]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The current date.
|
||||||
|
*/
|
||||||
|
def today(): F[LocalDate]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The date before today.
|
||||||
|
*/
|
||||||
|
def yesterday(): F[LocalDate]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The date after today.
|
||||||
|
*/
|
||||||
|
def tomorrow(): F[LocalDate]
|
||||||
388
modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala
Normal file
388
modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
package gs.std.v0.effect
|
||||||
|
|
||||||
|
import cats.Applicative
|
||||||
|
import cats.effect.Sync
|
||||||
|
import cats.syntax.all.*
|
||||||
|
import fs2.Stream
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
/** Random number generator.
|
||||||
|
*/
|
||||||
|
trait Rng[F[_]]:
|
||||||
|
|
||||||
|
/** Explicitly set or update the seed using the given 8 bytes.
|
||||||
|
*
|
||||||
|
* @param seed
|
||||||
|
* The seed value to assign or augment.
|
||||||
|
* @return
|
||||||
|
* This [[Rng]] instance.
|
||||||
|
*/
|
||||||
|
def updateSeed(seed: Long): F[Rng[F]]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next (unbounded) random `Int`.
|
||||||
|
*/
|
||||||
|
def nextInt(): F[Int]
|
||||||
|
|
||||||
|
/** Calculate a number bounded inclusively within 0 and some bound.
|
||||||
|
*
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The calculated number.
|
||||||
|
*/
|
||||||
|
def nextInt(bound: Int): F[Int]
|
||||||
|
|
||||||
|
/** Calculate a number bounded inclusively within some origin and some bound.
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* The origin.
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The calculated number.
|
||||||
|
*/
|
||||||
|
def nextInt(
|
||||||
|
origin: Int,
|
||||||
|
bound: Int
|
||||||
|
): F[Int]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next (unbounded) random `Long`.
|
||||||
|
*/
|
||||||
|
def nextLong(): F[Long]
|
||||||
|
|
||||||
|
/** Calculate a number bounded inclusively within 0 and some bound.
|
||||||
|
*
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The calculated number.
|
||||||
|
*/
|
||||||
|
def nextLong(bound: Long): F[Long]
|
||||||
|
|
||||||
|
/** Calculate a number bounded inclusively within some origin and some bound.
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* The origin.
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The calculated number.
|
||||||
|
*/
|
||||||
|
def nextLong(
|
||||||
|
origin: Long,
|
||||||
|
bound: Long
|
||||||
|
): F[Long]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next (unbounded) random `Double`.
|
||||||
|
*/
|
||||||
|
def nextDouble(): F[Double]
|
||||||
|
|
||||||
|
/** Calculate a number bounded inclusively within 0 and some bound.
|
||||||
|
*
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The calculated number.
|
||||||
|
*/
|
||||||
|
def nextDouble(bound: Double): F[Double]
|
||||||
|
|
||||||
|
/** Calculate a number bounded inclusively within some origin and some bound.
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* The origin.
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The calculated number.
|
||||||
|
*/
|
||||||
|
def nextDouble(
|
||||||
|
origin: Double,
|
||||||
|
bound: Double
|
||||||
|
): F[Double]
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The next randomly selected `true` or `false` value.
|
||||||
|
*/
|
||||||
|
def nextBoolean(): F[Boolean]
|
||||||
|
|
||||||
|
/** Produce an infinite stream of `Int` bounded inclusively within some origin
|
||||||
|
* and some bound.
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* The origin.
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The infinite stream of values.
|
||||||
|
*/
|
||||||
|
def nextInts(
|
||||||
|
origin: Int,
|
||||||
|
bound: Int
|
||||||
|
): Stream[F, Int]
|
||||||
|
|
||||||
|
/** Produce an infinite stream of `Long` bounded inclusively within some
|
||||||
|
* origin and some bound.
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* The origin.
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The infinite stream of values.
|
||||||
|
*/
|
||||||
|
def nextLongs(
|
||||||
|
origin: Long,
|
||||||
|
bound: Long
|
||||||
|
): Stream[F, Long]
|
||||||
|
|
||||||
|
/** Produce an infinite stream of `Double` bounded inclusively within some
|
||||||
|
* origin and some bound.
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* The origin.
|
||||||
|
* @param bound
|
||||||
|
* The bound.
|
||||||
|
* @return
|
||||||
|
* The infinite stream of values.
|
||||||
|
*/
|
||||||
|
def nextDoubles(
|
||||||
|
origin: Double,
|
||||||
|
bound: Double
|
||||||
|
): Stream[F, Double]
|
||||||
|
|
||||||
|
object Rng:
|
||||||
|
|
||||||
|
/** Instantiate [[Rng]] using a new `java.util.Random` instance.
|
||||||
|
*
|
||||||
|
* For secure random number generation, please refer to the `secure[F]`
|
||||||
|
* function.
|
||||||
|
*
|
||||||
|
* @param random
|
||||||
|
* The `java.util.Random` instance.
|
||||||
|
* @return
|
||||||
|
* The new [[Rng]].
|
||||||
|
*/
|
||||||
|
def default[F[_]: Sync]: Rng[F] =
|
||||||
|
default[F](new Random())
|
||||||
|
|
||||||
|
/** Instantiate [[Rng]] using the given random number generator.
|
||||||
|
*
|
||||||
|
* @param random
|
||||||
|
* The `java.util.Random` instance.
|
||||||
|
* @return
|
||||||
|
* The new [[Rng]].
|
||||||
|
*/
|
||||||
|
def default[F[_]: Sync](random: Random): Rng[F] =
|
||||||
|
new JavaRandom[F](random)
|
||||||
|
|
||||||
|
/** Instantiate [[Rng]] using a new `java.security.SecureRandom` instance.
|
||||||
|
*
|
||||||
|
* @param random
|
||||||
|
* The `java.security.SecureRandom` instance.
|
||||||
|
* @return
|
||||||
|
* The new [[Rng]].
|
||||||
|
*/
|
||||||
|
def secure[F[_]: Sync]: Rng[F] =
|
||||||
|
secure[F](new SecureRandom())
|
||||||
|
|
||||||
|
/** Instantiate [[Rng]] using the given secure random number generator.
|
||||||
|
*
|
||||||
|
* @param random
|
||||||
|
* The `java.security.SecureRandom` instance.
|
||||||
|
* @return
|
||||||
|
* The new [[Rng]].
|
||||||
|
*/
|
||||||
|
def secure[F[_]: Sync](random: SecureRandom): Rng[F] =
|
||||||
|
new JavaRandom[F](random)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* New instance of [[Rng]] that always returns `0` or `false`.
|
||||||
|
*/
|
||||||
|
def zero[F[_]: Applicative]: Rng[F] =
|
||||||
|
new Zero[F]
|
||||||
|
|
||||||
|
/** Implementation of [[Rng]] based on any given `java.util.Random` instance.
|
||||||
|
*
|
||||||
|
* @param random
|
||||||
|
* The underlying random number generator.
|
||||||
|
*/
|
||||||
|
final class JavaRandom[F[_]: Sync](random: Random) extends Rng[F]:
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def updateSeed(seed: Long): F[Rng[F]] =
|
||||||
|
Sync[F].delay(random.setSeed(seed)).as(this)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInt(): F[Int] =
|
||||||
|
Sync[F].delay(random.nextInt())
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInt(bound: Int): F[Int] =
|
||||||
|
Sync[F].delay(random.nextInt(bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInt(
|
||||||
|
origin: Int,
|
||||||
|
bound: Int
|
||||||
|
): F[Int] =
|
||||||
|
Sync[F].delay(random.nextInt(origin, bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLong(): F[Long] =
|
||||||
|
Sync[F].delay(random.nextLong())
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLong(bound: Long): F[Long] =
|
||||||
|
Sync[F].delay(random.nextLong(bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLong(
|
||||||
|
origin: Long,
|
||||||
|
bound: Long
|
||||||
|
): F[Long] =
|
||||||
|
Sync[F].delay(random.nextLong(origin, bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDouble(): F[Double] =
|
||||||
|
Sync[F].delay(random.nextDouble())
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDouble(bound: Double): F[Double] =
|
||||||
|
Sync[F].delay(random.nextDouble(bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDouble(
|
||||||
|
origin: Double,
|
||||||
|
bound: Double
|
||||||
|
): F[Double] =
|
||||||
|
Sync[F].delay(random.nextDouble(origin, bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextBoolean(): F[Boolean] =
|
||||||
|
Sync[F].delay(random.nextBoolean())
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInts(
|
||||||
|
origin: Int,
|
||||||
|
bound: Int
|
||||||
|
): Stream[F, Int] =
|
||||||
|
Stream.repeatEval(nextInt(origin, bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLongs(
|
||||||
|
origin: Long,
|
||||||
|
bound: Long
|
||||||
|
): Stream[F, Long] =
|
||||||
|
Stream.repeatEval(nextLong(origin, bound))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDoubles(
|
||||||
|
origin: Double,
|
||||||
|
bound: Double
|
||||||
|
): Stream[F, Double] =
|
||||||
|
Stream.repeatEval(nextDouble(origin, bound))
|
||||||
|
|
||||||
|
end JavaRandom
|
||||||
|
|
||||||
|
/** Implementation of [[Rng]] that always returns `0` or `false`.
|
||||||
|
*/
|
||||||
|
final class Zero[F[_]: Applicative] extends Rng[F]:
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def updateSeed(seed: Long): F[Rng[F]] = Applicative[F].pure(this)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInt(): F[Int] = Applicative[F].pure(0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInt(bound: Int): F[Int] = Applicative[F].pure(0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInt(
|
||||||
|
origin: Int,
|
||||||
|
bound: Int
|
||||||
|
): F[Int] = Applicative[F].pure(0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLong(): F[Long] = Applicative[F].pure(0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLong(bound: Long): F[Long] = Applicative[F].pure(0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLong(
|
||||||
|
origin: Long,
|
||||||
|
bound: Long
|
||||||
|
): F[Long] = Applicative[F].pure(0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDouble(): F[Double] = Applicative[F].pure(0.0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDouble(bound: Double): F[Double] = Applicative[F].pure(0.0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDouble(
|
||||||
|
origin: Double,
|
||||||
|
bound: Double
|
||||||
|
): F[Double] = Applicative[F].pure(0.0)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextBoolean(): F[Boolean] = Applicative[F].pure(false)
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextInts(
|
||||||
|
origin: Int,
|
||||||
|
bound: Int
|
||||||
|
): Stream[F, Int] =
|
||||||
|
Stream.repeatEval(Applicative[F].pure(0))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextLongs(
|
||||||
|
origin: Long,
|
||||||
|
bound: Long
|
||||||
|
): Stream[F, Long] =
|
||||||
|
Stream.repeatEval(Applicative[F].pure(0))
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def nextDoubles(
|
||||||
|
origin: Double,
|
||||||
|
bound: Double
|
||||||
|
): Stream[F, Double] =
|
||||||
|
Stream.repeatEval(Applicative[F].pure(0.0))
|
||||||
|
|
||||||
|
end Zero
|
||||||
|
|
||||||
|
end Rng
|
||||||
BIN
modules/effect/src/test/resources/sha256-input
Normal file
BIN
modules/effect/src/test/resources/sha256-input
Normal file
Binary file not shown.
118
modules/io/src/main/scala/src/gs/std/v0/io/Files.scala
Normal file
118
modules/io/src/main/scala/src/gs/std/v0/io/Files.scala
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
package gs.std.v0.io
|
||||||
|
|
||||||
|
import cats.effect.Resource
|
||||||
|
import cats.effect.Sync
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.OpenOption
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
|
/** Common operations on files.
|
||||||
|
*
|
||||||
|
* Includes wrappers around some `java.nio.file.Files` functionality.
|
||||||
|
*
|
||||||
|
* For streaming operations, just use the `fs2.io` library.
|
||||||
|
*/
|
||||||
|
object Files:
|
||||||
|
|
||||||
|
def readFileAsString[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): F[String] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.readString(path, charset))
|
||||||
|
|
||||||
|
def readFileAsBytes[F[_]: Sync](path: Path): F[Array[Byte]] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.readAllBytes(path))
|
||||||
|
|
||||||
|
def readFileAsLines[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): F[List[String]] =
|
||||||
|
Sync[F].delay(
|
||||||
|
java.nio.file.Files.readAllLines(path, charset).asScala.toList
|
||||||
|
)
|
||||||
|
|
||||||
|
def overwriteFileWithString[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
data: String
|
||||||
|
): F[Unit] =
|
||||||
|
Sync[F].delay(
|
||||||
|
java.nio.file.Files.writeString(
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def overwriteFileWithBytes[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
data: Array[Byte]
|
||||||
|
): F[Unit] =
|
||||||
|
Sync[F].delay(
|
||||||
|
java.nio.file.Files.write(
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def appendFileWithString[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
data: String
|
||||||
|
): F[Unit] =
|
||||||
|
Sync[F].delay(
|
||||||
|
java.nio.file.Files.writeString(
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.APPEND
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def appendFileWithBytes[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
data: Array[Byte]
|
||||||
|
): F[Unit] =
|
||||||
|
Sync[F].delay(
|
||||||
|
java.nio.file.Files
|
||||||
|
.write(path, data, StandardOpenOption.WRITE, StandardOpenOption.APPEND)
|
||||||
|
)
|
||||||
|
|
||||||
|
def isRegularFile[F[_]: Sync](path: Path): F[Boolean] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.isRegularFile(path))
|
||||||
|
|
||||||
|
def isReadable[F[_]: Sync](path: Path): F[Boolean] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.isReadable(path))
|
||||||
|
|
||||||
|
def isWritable[F[_]: Sync](path: Path): F[Boolean] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.isWritable(path))
|
||||||
|
|
||||||
|
def isDirectory[F[_]: Sync](path: Path): F[Boolean] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.isDirectory(path))
|
||||||
|
|
||||||
|
def isExecutable[F[_]: Sync](path: Path): F[Boolean] =
|
||||||
|
Sync[F].delay(java.nio.file.Files.isExecutable(path))
|
||||||
|
|
||||||
|
def openInputStream[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
openOptions: OpenOption*
|
||||||
|
): Resource[F, InputStream] =
|
||||||
|
Resource.make(
|
||||||
|
Sync[F].delay(java.nio.file.Files.newInputStream(path, openOptions*))
|
||||||
|
)(inputStream => Sync[F].delay(inputStream.close()))
|
||||||
|
|
||||||
|
def openOutputStream[F[_]: Sync](
|
||||||
|
path: Path,
|
||||||
|
openOptions: OpenOption*
|
||||||
|
): Resource[F, OutputStream] =
|
||||||
|
Resource.make(
|
||||||
|
Sync[F].delay(java.nio.file.Files.newOutputStream(path, openOptions*))
|
||||||
|
)(outputStream => Sync[F].delay(outputStream.close()))
|
||||||
|
|
||||||
|
end Files
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a blob -- some array of bytes.
|
|
||||||
*
|
|
||||||
* @param data The underlying data.
|
|
||||||
*/
|
|
||||||
final class Blob(private val data: Array[Byte]) extends IndexedSeq[Byte]:
|
|
||||||
|
|
||||||
/** @inheritDocs */
|
|
||||||
override def apply(i: Int): Byte = byteAt(i)
|
|
||||||
|
|
||||||
/** @inheritDocs */
|
|
||||||
override def length: Int = data.length
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The number of bytes in this blob, expressed as a count.
|
|
||||||
*/
|
|
||||||
def numberOfBytes: ByteCount = ByteCount(data.length)
|
|
||||||
|
|
||||||
/** @inheritDocs */
|
|
||||||
override def equals(obj: Any): Boolean =
|
|
||||||
obj match
|
|
||||||
case other: Blob => data.sameElements(other.data)
|
|
||||||
case other: Array[Byte] => data.sameElements(other)
|
|
||||||
case _ => false
|
|
||||||
|
|
||||||
/** @inheritDocs */
|
|
||||||
override def hashCode(): Int = data.hashCode()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the byte at the given index.
|
|
||||||
*
|
|
||||||
* @param index The index.
|
|
||||||
* @return The byte stored at the given index.
|
|
||||||
*/
|
|
||||||
def byteAt(index: Int): Byte = data.apply(index)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return This byte array, encoded as a base64 string.
|
|
||||||
*/
|
|
||||||
def base64(): String = Base64.getEncoder().encodeToString(data)
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type for some number of bytes (>= 0).
|
|
||||||
*/
|
|
||||||
opaque type ByteCount = Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type for some number of bytes (>= 0).
|
|
||||||
*/
|
|
||||||
object ByteCount:
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 0 bytes.
|
|
||||||
*/
|
|
||||||
final val Zero: ByteCount = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1,000 bytes.
|
|
||||||
*/
|
|
||||||
final val OneKilobyte: ByteCount = 1000
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1,000,000 bytes.
|
|
||||||
*/
|
|
||||||
final val OneMegabyte: ByteCount = 1000000
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1,000,000,000 bytes.
|
|
||||||
*/
|
|
||||||
final val OneGigabyte: ByteCount = 1000000000
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Express the given number as a byte count. All values are normalized to the
|
|
||||||
* absolute value -- negative values are coerced to positive.
|
|
||||||
*
|
|
||||||
* @param value The input integer.
|
|
||||||
* @return The [[ByteCount]] instance.
|
|
||||||
*/
|
|
||||||
def apply(value: Long): ByteCount = Math.abs(value)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1 kilobyte = 1,000 bytes
|
|
||||||
*
|
|
||||||
* @param kb The number of kilobytes.
|
|
||||||
* @return The number of bytes.
|
|
||||||
*/
|
|
||||||
def fromKilobytes(kb: Long): ByteCount =
|
|
||||||
Math.abs(kb) * 1000L
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1 megabyte = 1,000,000 bytes
|
|
||||||
*
|
|
||||||
* @param mb The number of megabytes.
|
|
||||||
* @return The number of bytes.
|
|
||||||
*/
|
|
||||||
def fromMegabytes(mb: Long): ByteCount =
|
|
||||||
Math.abs(mb) * 1000000L
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1 gigabyte = 1,000,000,000 bytes
|
|
||||||
*
|
|
||||||
* @param mb The number of gigabytes.
|
|
||||||
* @return The number of bytes.
|
|
||||||
*/
|
|
||||||
def fromGigabytes(gb: Long): ByteCount =
|
|
||||||
Math.abs(gb) * 1000000000
|
|
||||||
|
|
||||||
given CanEqual[ByteCount, ByteCount] = CanEqual.derived
|
|
||||||
|
|
||||||
given Ordering[ByteCount] with
|
|
||||||
/** @inheritDocs */
|
|
||||||
def compare(x: ByteCount, y: ByteCount): Int =
|
|
||||||
if x > y then 1 else if x == y then 0 else -1
|
|
||||||
|
|
||||||
extension (byteCount: ByteCount)
|
|
||||||
/**
|
|
||||||
* @return The underlying `Long`.
|
|
||||||
*/
|
|
||||||
def unwrap(): Long = byteCount
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The underlying `Long`.
|
|
||||||
*/
|
|
||||||
def toLong(): Long = byteCount
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1 kilobyte = 1,000 bytes
|
|
||||||
*
|
|
||||||
* @return The number of kilobytes represented by this count.
|
|
||||||
*/
|
|
||||||
def toKilobytes(): Double = byteCount / 1000.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1 megabyte = 1,000,000 bytes.
|
|
||||||
*
|
|
||||||
* @return The number of megabytes represented by this count.
|
|
||||||
*/
|
|
||||||
def toMegabytes(): Double = byteCount / 1000000.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1 gigabyte = 1,000,000,000 bytes.
|
|
||||||
*
|
|
||||||
* @return The number of gigabytes represented by this count.
|
|
||||||
*/
|
|
||||||
def toGigabytes(): Double = byteCount / 1000000000.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add some count to this one.
|
|
||||||
*
|
|
||||||
* @param that The number to add.
|
|
||||||
* @return The sum of the numbers.
|
|
||||||
*/
|
|
||||||
def +(that: ByteCount): ByteCount = byteCount + that
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multiply this count by some other count.
|
|
||||||
*
|
|
||||||
* @param that The number to multiply by.
|
|
||||||
* @return The product of the numbers.
|
|
||||||
*/
|
|
||||||
def *(that: ByteCount): ByteCount = byteCount * that
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this value is the same as some number.
|
|
||||||
*
|
|
||||||
* @param value The number to compare against.
|
|
||||||
* @return True if the values are equal, false otherwise.
|
|
||||||
*/
|
|
||||||
def equal(value: Int): Boolean = byteCount == value.toLong
|
|
||||||
|
|
||||||
end ByteCount
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type that represents the instant something was created.
|
|
||||||
*/
|
|
||||||
opaque type CreatedAt = Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type that represents the instant something was created.
|
|
||||||
*/
|
|
||||||
object CreatedAt:
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate a new [[CreatedAt]] from the given `java.time.Instant`.
|
|
||||||
*
|
|
||||||
* @param value The value to semantically represent.
|
|
||||||
* @return The new [[CreatedAt]].
|
|
||||||
*/
|
|
||||||
def apply(value: Instant): CreatedAt = value
|
|
||||||
|
|
||||||
given CanEqual[CreatedAt, CreatedAt] = CanEqual.derived
|
|
||||||
|
|
||||||
given Ordering[CreatedAt] = Ordering[Instant]
|
|
||||||
|
|
||||||
extension (createdAt: Instant)
|
|
||||||
/**
|
|
||||||
* @return The underlying `java.time.Instant`.
|
|
||||||
*/
|
|
||||||
def unwrap(): Instant = createdAt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The underlying `java.time.Instant`.
|
|
||||||
*/
|
|
||||||
def toInstant(): Instant = createdAt
|
|
||||||
|
|
||||||
end CreatedAt
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for byte decoding from encoded String formats.
|
|
||||||
*/
|
|
||||||
trait Decoder[-A <: EncodedString]:
|
|
||||||
/**
|
|
||||||
* Decode an input string to an array of bytes.
|
|
||||||
*
|
|
||||||
* @param input The input to decode.
|
|
||||||
* @return The decoded byte array.
|
|
||||||
*/
|
|
||||||
def decode(input: A): Array[Byte]
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type representing a MD5 hash.
|
|
||||||
*/
|
|
||||||
opaque type MD5 = Array[Byte]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type representing a MD5 hash.
|
|
||||||
*/
|
|
||||||
object MD5:
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MD5 hashes are exactly 16 bytes.
|
|
||||||
*/
|
|
||||||
final val NumberOfBytes: ByteCount = ByteCount(16)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The algorithm name is "MD5".
|
|
||||||
*/
|
|
||||||
final val Algorithm: String = "MD5"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate a [[MD5]] from the given byte array. This function does not
|
|
||||||
* know whether the array is actually a calculated hash.
|
|
||||||
*
|
|
||||||
* Typically used for loading pre-validated hashes (e.g. from a database).
|
|
||||||
*
|
|
||||||
* @param bytes The bytes - must contain exactly 16 bytes.
|
|
||||||
* @return The new [[MD5]] instance.
|
|
||||||
*/
|
|
||||||
def fromBytes(bytes: Array[Byte]): MD5 =
|
|
||||||
if NumberOfBytes.equal(bytes.length) then
|
|
||||||
bytes
|
|
||||||
else
|
|
||||||
throw IllegalArgumentException(s"MD5 values must be exactly $NumberOfBytes bytes.")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the MD5 hash for the given byte array.
|
|
||||||
*
|
|
||||||
* @param data The byte array.
|
|
||||||
* @return The calculated [[MD5]].
|
|
||||||
*/
|
|
||||||
def calculate(data: Array[Byte]): MD5 =
|
|
||||||
MessageDigest.getInstance(Algorithm).digest(data)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Calculate the MD5 hash for the given string.
|
|
||||||
*
|
|
||||||
* @param data The string data.
|
|
||||||
* @param charset The character set of the string. Defaults to UTF-8.
|
|
||||||
* @return The calculated [[MD5]].
|
|
||||||
*/
|
|
||||||
def calculate(data: String, charset: Charset = StandardCharsets.UTF_8): MD5 =
|
|
||||||
calculate(data.getBytes(charset))
|
|
||||||
|
|
||||||
extension (md5: MD5)
|
|
||||||
/**
|
|
||||||
* @return The underlying byte array.
|
|
||||||
*/
|
|
||||||
def unwrap(): Array[Byte] = md5
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The underlying byte array.
|
|
||||||
*/
|
|
||||||
def toBytes(): Array[Byte] = md5
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the byte at the given index (0 to 15).
|
|
||||||
*
|
|
||||||
* Throws an exception if an out-of-bound index is given.
|
|
||||||
*
|
|
||||||
* @param index The byte index (0 to 15).
|
|
||||||
* @return The byte at the specified index.
|
|
||||||
*/
|
|
||||||
def getByte(index: Int): Byte =
|
|
||||||
if index < 0 || index >= NumberOfBytes.unwrap() then
|
|
||||||
throw IndexOutOfBoundsException(s"Index $index out of MD5 bound of $NumberOfBytes bytes.")
|
|
||||||
else
|
|
||||||
md5.apply(index)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if this hash is the same as some other hash. Compares each byte
|
|
||||||
* in order.
|
|
||||||
*
|
|
||||||
* @param other The [[MD5]] to compare against.
|
|
||||||
* @return True if the hashes are identical, false otherwise.
|
|
||||||
*/
|
|
||||||
def isSame(other: MD5): Boolean = md5.sameElements(other)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return This hash encoded to a Base64 string.
|
|
||||||
*/
|
|
||||||
def base64(): EncodedString = Base64Encoder.encode(md5)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return This hash encoded to a Hexadecimal string.
|
|
||||||
*/
|
|
||||||
def hex(): EncodedString = HexEncoder.encode(md5)
|
|
||||||
|
|
||||||
end MD5
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type for the natural numbers (including 0).
|
|
||||||
*/
|
|
||||||
opaque type Nat = Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type for the natural numbers (including 0).
|
|
||||||
*/
|
|
||||||
object Nat:
|
|
||||||
|
|
||||||
sealed trait Invalid
|
|
||||||
object Invalid extends Invalid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number 0.
|
|
||||||
*/
|
|
||||||
final val Zero: Nat = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number 1.
|
|
||||||
*/
|
|
||||||
final val One: Nat = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Express the given integer as a natural number.
|
|
||||||
*
|
|
||||||
* Throws an `IllegalArgumentException` if a negative value is given as input.
|
|
||||||
*
|
|
||||||
* @param value The input integer.
|
|
||||||
* @return The [[Nat]] instance.
|
|
||||||
*/
|
|
||||||
def apply(value: Int): Nat =
|
|
||||||
if value >= 0 then
|
|
||||||
value
|
|
||||||
else
|
|
||||||
throw new IllegalArgumentException("Nat values must be 0 or greater.")
|
|
||||||
|
|
||||||
def validate(value: Int): Either[Invalid, Nat] =
|
|
||||||
if value >= 0 then Right(value) else Left(Invalid)
|
|
||||||
|
|
||||||
given CanEqual[Nat, Nat] = CanEqual.derived
|
|
||||||
|
|
||||||
given Ordering[Nat] with
|
|
||||||
/** @inheritDocs */
|
|
||||||
def compare(x: Nat, y: Nat): Int = x - y
|
|
||||||
|
|
||||||
extension (nat: Nat)
|
|
||||||
/**
|
|
||||||
* @return The underlying integer.
|
|
||||||
*/
|
|
||||||
def unwrap(): Int = nat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The next integer.
|
|
||||||
*/
|
|
||||||
def next(): Nat = nat + 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The next integer.
|
|
||||||
*/
|
|
||||||
def increment(): Nat = nat + 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add some natural number to this one.
|
|
||||||
*
|
|
||||||
* @param that The number to add.
|
|
||||||
* @return The sum of the numbers.
|
|
||||||
*/
|
|
||||||
def +(that: Nat): Nat = nat + that
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multiply this natural number by some other natural number.
|
|
||||||
*
|
|
||||||
* @param that The number to multiply by.
|
|
||||||
* @return The product of the numbers.
|
|
||||||
*/
|
|
||||||
def *(that: Nat): Nat = nat * that
|
|
||||||
|
|
||||||
end Nat
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type representing a SHA-256 hash.
|
|
||||||
*/
|
|
||||||
opaque type SHA256 = Array[Byte]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type representing a SHA-256 hash.
|
|
||||||
*/
|
|
||||||
object SHA256:
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SHA-256 hashes are exactly 32 bytes.
|
|
||||||
*/
|
|
||||||
final val NumberOfBytes: ByteCount = ByteCount(32)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The algorithm name is "SHA-256".
|
|
||||||
*/
|
|
||||||
final val Algorithm: String = "SHA-256"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate a [[SHA256]] from the given byte array. This function does not
|
|
||||||
* know whether the array is actually a calculated hash.
|
|
||||||
*
|
|
||||||
* Typically used for loading pre-validated hashes (e.g. from a database).
|
|
||||||
*
|
|
||||||
* @param bytes The bytes - must contain exactly 32 bytes.
|
|
||||||
* @return The new [[SHA256]] instance.
|
|
||||||
*/
|
|
||||||
def fromBytes(bytes: Array[Byte]): SHA256 =
|
|
||||||
if NumberOfBytes.equal(bytes.length) then
|
|
||||||
bytes
|
|
||||||
else
|
|
||||||
throw IllegalArgumentException(s"SHA-256 values must be exactly $NumberOfBytes bytes.")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the SHA-256 hash for the given byte array.
|
|
||||||
*
|
|
||||||
* @param data The byte array.
|
|
||||||
* @return The calculated [[SHA256]].
|
|
||||||
*/
|
|
||||||
def calculate(data: Array[Byte]): SHA256 =
|
|
||||||
MessageDigest.getInstance(Algorithm).digest(data)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the SHA-256 hash for the given string.
|
|
||||||
*
|
|
||||||
* @param data The string data.
|
|
||||||
* @param charset The character set of the string. Defaults to UTF-8.
|
|
||||||
* @return The calculated [[SHA256]].
|
|
||||||
*/
|
|
||||||
def calculate(data: String, charset: Charset = StandardCharsets.UTF_8): SHA256 =
|
|
||||||
calculate(data.getBytes(charset))
|
|
||||||
|
|
||||||
extension (sha: SHA256)
|
|
||||||
/**
|
|
||||||
* @return The underlying byte array.
|
|
||||||
*/
|
|
||||||
def unwrap(): Array[Byte] = sha
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The underlying byte array.
|
|
||||||
*/
|
|
||||||
def toBytes(): Array[Byte] = sha
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the byte at the given index (0 to 31).
|
|
||||||
*
|
|
||||||
* Throws an exception if an out-of-bound index is given.
|
|
||||||
*
|
|
||||||
* @param index The byte index (0 to 31).
|
|
||||||
* @return The byte at the specified index.
|
|
||||||
*/
|
|
||||||
def getByte(index: Int): Byte =
|
|
||||||
if index < 0 || index >= NumberOfBytes.unwrap() then
|
|
||||||
throw IndexOutOfBoundsException(s"Index $index out of SHA-256 bound of $NumberOfBytes bytes.")
|
|
||||||
else
|
|
||||||
sha.apply(index)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if this hash is the same as some other hash. Compares each byte
|
|
||||||
* in order.
|
|
||||||
*
|
|
||||||
* @param other The [[SHA256]] to compare against.
|
|
||||||
* @return True if the hashes are identical, false otherwise.
|
|
||||||
*/
|
|
||||||
def isSame(other: SHA256): Boolean = sha.sameElements(other)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return This hash encoded to a Base64 string.
|
|
||||||
*/
|
|
||||||
def base64(): EncodedString = Base64Encoder.encode(sha)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return This hash encoded to a Hexadecimal string.
|
|
||||||
*/
|
|
||||||
def hex(): EncodedString = HexEncoder.encode(sha)
|
|
||||||
|
|
||||||
end SHA256
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type for collection sizes. Values are guaranteed to be 0 or greater.
|
|
||||||
*/
|
|
||||||
opaque type Size = Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type for collection sizes. Values are guaranteed to be 0 or greater.
|
|
||||||
*/
|
|
||||||
object Size:
|
|
||||||
|
|
||||||
sealed trait Invalid
|
|
||||||
object Invalid extends Invalid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The size 0.
|
|
||||||
*/
|
|
||||||
final val Zero: Size = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The size 1.
|
|
||||||
*/
|
|
||||||
final val One: Size = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Express the given integer as a size.
|
|
||||||
*
|
|
||||||
* Throws an `IllegalArgumentException` if a negative value is given as input.
|
|
||||||
*
|
|
||||||
* @param value The input integer.
|
|
||||||
* @return The [[Size]] instance.
|
|
||||||
*/
|
|
||||||
def apply(value: Int): Size =
|
|
||||||
if value >= 0 then
|
|
||||||
value
|
|
||||||
else
|
|
||||||
throw new IllegalArgumentException("Size values must be 0 or greater.")
|
|
||||||
|
|
||||||
def validate(value: Int): Either[Invalid, Size] =
|
|
||||||
if value >= 0 then Right(value) else Left(Invalid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Express the size of any collection.
|
|
||||||
*
|
|
||||||
* @param iter The collection.
|
|
||||||
* @return The size of the collection.
|
|
||||||
*/
|
|
||||||
def of(iter: Iterable[?]): Size = iter.size
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Express the size of any array.
|
|
||||||
*
|
|
||||||
* @param arr The array.
|
|
||||||
* @return The size of the array.
|
|
||||||
*/
|
|
||||||
def of(arr: Array[?]): Size = arr.length
|
|
||||||
|
|
||||||
given CanEqual[Size, Size] = CanEqual.derived
|
|
||||||
|
|
||||||
given Ordering[Size] with
|
|
||||||
/** @inheritDocs */
|
|
||||||
def compare(x: Size, y: Size): Int = x - y
|
|
||||||
|
|
||||||
extension (size: Size)
|
|
||||||
/**
|
|
||||||
* @return The underlying integer.
|
|
||||||
*/
|
|
||||||
def unwrap(): Int = size
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The next value.
|
|
||||||
*/
|
|
||||||
def next(): Size = size + 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The next value.
|
|
||||||
*/
|
|
||||||
def increment(): Size = size + 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add some size to this one.
|
|
||||||
*
|
|
||||||
* @param that The number to add.
|
|
||||||
* @return The sum of the numbers.
|
|
||||||
*/
|
|
||||||
def +(that: Size): Size = size + that
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multiply this size by some other size.
|
|
||||||
*
|
|
||||||
* @param that The number to multiply by.
|
|
||||||
* @return The product of the numbers.
|
|
||||||
*/
|
|
||||||
def *(that: Size): Size = size * that
|
|
||||||
|
|
||||||
end Size
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package gs.std.v0
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type that represents the instant something was updated.
|
|
||||||
*/
|
|
||||||
opaque type UpdatedAt = Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opaque type that represents the instant something was updated.
|
|
||||||
*/
|
|
||||||
object UpdatedAt:
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate a new [[UpdatedAt]] from the given `java.time.Instant`.
|
|
||||||
*
|
|
||||||
* @param value The value to semantically represent.
|
|
||||||
* @return The new [[UpdatedAt]].
|
|
||||||
*/
|
|
||||||
def apply(value: Instant): UpdatedAt = value
|
|
||||||
|
|
||||||
given CanEqual[UpdatedAt, UpdatedAt] = CanEqual.derived
|
|
||||||
|
|
||||||
given Ordering[UpdatedAt] = Ordering[Instant]
|
|
||||||
|
|
||||||
extension (updatedAt: Instant)
|
|
||||||
/**
|
|
||||||
* @return The underlying `java.time.Instant`.
|
|
||||||
*/
|
|
||||||
def unwrap(): Instant = updatedAt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The underlying `java.time.Instant`.
|
|
||||||
*/
|
|
||||||
def toInstant(): Instant = updatedAt
|
|
||||||
|
|
||||||
end UpdatedAt
|
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue