davenverse / shellfish

Shell Scripting for Cats-Effect
MIT License
4 stars 4 forks source link

Implementing `Files` operations #63

Open TonioGela opened 1 month ago

TonioGela commented 1 month ago

The first step after #55 should be to add to the library the functionalities available in Files, keeping in mind that we decided to hardcode to IO, to do not use the tagless final style, and the end goal is to provide a layer of fluent APIs.

The rough way I've pictured this in my head is to create an object call that extends Files[IO] and that takes APIs like:

def readAll(path: Path): Stream[F, Byte]

and re-exports them as their "stricter, IO hardcoded" version:

def readAll(path: Path): IO[Array[Byte]]

Let's keep in mind that to make things "really fluent", we can also think of a way to add a syntax over Path, so that we could be able to call the same APIs as

(p: Path).readAll: IO[Array[Byte]]

that is something that fits well (IMHO) in a for comprehension where files are read/created/etc.

Let's also keep in mind that possibly not all the Files methods will need to be exposed (at least at the beginning).

If we need some inspiration to add some helpful method, we might have a look at java.nio.Files or os-lib.

TonioGela commented 1 month ago

Thinking out loud here. Theoretically to write/append to a file we should call this API:

def writeAll(path: Path, flags: Flags): Pipe[F, Byte, Nothing]

with either Flags.Write or Flags.Append and then piping a Stream[F, Byte] in it.

How about splitting it into a few methods like these?

(path: Path).write(bytes: Array[Byte]): IO[Unit]
(path: Path).writeUtf8(s: String): IO[Unit]
(path: Path).append(bytes: Array[Byte]): IO[Unit]
(path: Path).appendUtf8(s: String): IO[Unit]
TonioGela commented 1 month ago

I would also love to hear everybody's opinion about having some kind of file-contents'-handle like structure, a generalization of this 👇

case class Score(name: String, score: Int):
    def to: String = s"$name:$score"

object Score:
    def from(s:String)(line: Long): IO[Score] = s.split(':') match
        case Array(name, score) => IO(Score(name, score.toInt))
        case _ => IO.raiseError(new Exception(s"Malformed score $s at line $line"))

def readScores: IO[List[Score]] = Files[IO].readUtf8Lines(Path("/tmp/score"))
    .filterNot(_.isBlank).zipWithIndex.evalMap((s,i) => Score.from(s)(i)).compile.toList

def saveScores(scores: List[Score]): IO[Unit] = Stream.emits(scores).map(_.to)
    .through(Files[IO].writeUtf8Lines(Path("/tmp/.config/score"))).compile.drain

def scoreResource: Resource[IO, Ref[IO, List[Score]]] = Resource(
    readScores.flatMap(Ref.of).fproduct(_.get.flatMap(saveScores))
)

that you can use in a Resource like fashion:

scoreResource.use(_.update(Score("tonio", 100) :: _))
mpilquist commented 1 month ago

Aside: I would avoid use of Array[Byte] in the APIs and use Chunk[Byte] instead.

armanbilge commented 1 month ago

use Chunk[Byte] instead.

My vote would be for ByteVector. It directly offers more useful APIs for working with Bytes e.g. decoding into a String. See also my reasoning in https://github.com/http4s/http4s/pull/6528.

lenguyenthanh commented 1 month ago

Let's keep in mind that to make things "really fluent", we can also think of a way to add a syntax over Path, so that we could be able to call the same APIs as

(p: Path).readAll: IO[Array[Byte]]

This syntax is a bit awkward imho, ex:

p"$temp_dir/log.txt".readAll
// or
"user/hom/dir/other_dir/file.txt".readAll

So I prefer something like:

Files.readAll(path: Path): IO[ByteVector]
// or
Shellfish.readAll(path: Path): IO[ByteVector]
// or
Shellfish.files.readAll(path: Path): IO[ByteVector] // os lib style
TonioGela commented 1 month ago

I was more thinking of something like

val path = Path("/path/to/directory")
for
 lines <- path.readUtf8Lines
 uppercased = lines.map(_.toUpperCase)
 newPath =  path + "_uppercased"
 _ <- newPath.writeUtf8Lines(uppercased)
yield ()