From eedf6e8f043a6ae92c3a70d19c55176d7e1ffee0 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Thu, 30 Apr 2026 22:06:35 -0500 Subject: [PATCH] Expanding library to effectful operations. --- build.sbt | 13 +- .../main/scala/src/gs/std/v0/effect/RNG.scala | 168 ++++++++++++++++++ .../main/scala/src/gs/std/v0/io/Files.scala | 117 ++++++++++++ 3 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala create mode 100644 modules/io/src/main/scala/src/gs/std/v0/io/Files.scala diff --git a/build.sbt b/build.sbt index d9b5543..07da615 100644 --- a/build.sbt +++ b/build.sbt @@ -45,7 +45,7 @@ lazy val testSettings = Seq( lazy val `gs-std` = project .in(file(".")) - .aggregate(core, io, effect, stream) + .aggregate(core, io, effect) .settings(sharedSettings) .settings(testSettings) .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") @@ -75,11 +75,6 @@ lazy val effect = project .settings(sharedSettings) .settings(testSettings) .settings(name := s"${gsProjectName.value}-effect-v${semVerMajor.value}") - .settings(libraryDependencies ++= Seq(Deps.Cats.Core, Deps.Cats.Effect)) - -lazy val stream = project - .in(file("modules/stream")) - .settings(sharedSettings) - .settings(testSettings) - .settings(name := s"${gsProjectName.value}-stream-v${semVerMajor.value}") - .settings(libraryDependencies ++= Seq(Deps.Fs2.Core)) + .settings( + libraryDependencies ++= Seq(Deps.Cats.Core, Deps.Cats.Effect, Deps.Fs2.Core) + ) diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala new file mode 100644 index 0000000..34e9dd1 --- /dev/null +++ b/modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala @@ -0,0 +1,168 @@ +package gs.std.v0.effect + +import cats.Applicative +import cats.effect.Sync +import fs2.Stream +import java.security.SecureRandom +import java.util.Random + +trait RNG[F[_]]: + def nextInt(): F[Int] + def nextInt(bound: Int): F[Int] + + def nextInt( + origin: Int, + bound: Int + ): F[Int] + + def nextLong(): F[Long] + def nextLong(bound: Long): F[Long] + + def nextLong( + origin: Long, + bound: Long + ): F[Long] + + def nextDouble(): F[Double] + def nextDouble(bound: Double): F[Double] + + def nextDouble( + origin: Double, + bound: Double + ): F[Double] + + def nextBoolean(): F[Boolean] + + def nextInts( + origin: Int, + bound: Int + ): Stream[F, Int] + + def nextLongs( + origin: Long, + bound: Long + ): Stream[F, Long] + + def nextDoubles( + origin: Double, + bound: Double + ): Stream[F, Double] + +object RNG: + + def secure[F[_]: Sync]: RNG[F] = + new JavaRandom[F](new SecureRandom()) + + final class JavaRandom[F[_]: Sync](random: Random) extends RNG[F]: + + override def nextInt(): F[Int] = + Sync[F].delay(random.nextInt()) + + override def nextInt(bound: Int): F[Int] = + Sync[F].delay(random.nextInt(bound)) + + override def nextInt( + origin: Int, + bound: Int + ): F[Int] = + Sync[F].delay(random.nextInt(origin, bound)) + + override def nextLong(): F[Long] = + Sync[F].delay(random.nextLong()) + + override def nextLong(bound: Long): F[Long] = + Sync[F].delay(random.nextLong(bound)) + + override def nextLong( + origin: Long, + bound: Long + ): F[Long] = + Sync[F].delay(random.nextLong(origin, bound)) + + override def nextDouble(): F[Double] = + Sync[F].delay(random.nextDouble()) + + override def nextDouble(bound: Double): F[Double] = + Sync[F].delay(random.nextDouble(bound)) + + override def nextDouble( + origin: Double, + bound: Double + ): F[Double] = + Sync[F].delay(random.nextDouble(origin, bound)) + + override def nextBoolean(): F[Boolean] = + Sync[F].delay(random.nextBoolean()) + + override def nextInts( + origin: Int, + bound: Int + ): Stream[F, Int] = + Stream.repeatEval(nextInt(origin, bound)) + + override def nextLongs( + origin: Long, + bound: Long + ): Stream[F, Long] = + Stream.repeatEval(nextLong(origin, bound)) + + override def nextDoubles( + origin: Double, + bound: Double + ): Stream[F, Double] = + Stream.repeatEval(nextDouble(origin, bound)) + + end JavaRandom + + final class Zero[F[_]: Applicative] extends RNG[F]: + + override def nextInt(): F[Int] = Applicative[F].pure(0) + + override def nextInt(bound: Int): F[Int] = Applicative[F].pure(0) + + override def nextInt( + origin: Int, + bound: Int + ): F[Int] = Applicative[F].pure(0) + + override def nextLong(): F[Long] = Applicative[F].pure(0) + + override def nextLong(bound: Long): F[Long] = Applicative[F].pure(0) + + override def nextLong( + origin: Long, + bound: Long + ): F[Long] = Applicative[F].pure(0) + + override def nextDouble(): F[Double] = Applicative[F].pure(0.0) + + override def nextDouble(bound: Double): F[Double] = Applicative[F].pure(0.0) + + override def nextDouble( + origin: Double, + bound: Double + ): F[Double] = Applicative[F].pure(0.0) + + override def nextBoolean(): F[Boolean] = Applicative[F].pure(false) + + override def nextInts( + origin: Int, + bound: Int + ): Stream[F, Int] = + Stream.repeatEval(Applicative[F].pure(0)) + + override def nextLongs( + origin: Long, + bound: Long + ): Stream[F, Long] = + Stream.repeatEval(Applicative[F].pure(0)) + + override def nextDoubles( + origin: Double, + bound: Double + ): Stream[F, Double] = + Stream.repeatEval(Applicative[F].pure(0.0)) + + end Zero + +end RNG 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 new file mode 100644 index 0000000..43e5e02 --- /dev/null +++ b/modules/io/src/main/scala/src/gs/std/v0/io/Files.scala @@ -0,0 +1,117 @@ +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, wrapped using Cats Effect. If you want to Stream + * files, the `fs2-io` package is the place to go. + * + * Includes wrappers around some `java.nio.file.Files` functionality. + */ +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