# `VFS`
[🔗](https://github.com/ivarvong/vfs/blob/v0.1.0/lib/vfs.ex#L1)

Protocol-based virtual filesystem.

`VFS` is both a *mount table* (a struct holding a list of
`{mountpoint, backend}` pairs) and the *public API* for working with
virtual filesystems. The struct itself implements `VFS.Mountable`, so
mount tables nest naturally — you can mount a `%VFS{}` inside another
`%VFS{}` for namespacing.

The data-flow ops in this module — `read_file/2`, `stream_read/3`,
`write_file/4`, `mkdir/3`, `rm/3`, `walk/3`, `materialize/2` — are
wrapped in `:telemetry.span/3` so consumers can attach OpenTelemetry,
log, or metric handlers. The cheap lookups (`exists?/2`, `stat/2`,
`readdir/2`, `capabilities/1`) are not instrumented. The "Telemetry
events" section below is the full, exact contract.

## Quick tour

    iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/repo/README.md" => "hello\n", "/tmp/scratch" => ""}))
    iex> {:ok, "hello\n", fs} = VFS.read_file(fs, "/repo/README.md")
    iex> {:ok, fs} = VFS.write_file(fs, "/tmp/scratch", "world\n")
    iex> {:ok, "world\n", _fs} = VFS.read_file(fs, "/tmp/scratch")
    iex> :ok
    :ok

## State threading

Every op returns the (possibly updated) `%VFS{}` as the last element of
its success tuple. Threading it forward preserves lazy backend caches.
See `VFS.Mountable` for the full contract.

## Telemetry events

All under the `[:vfs, _, _]` prefix:

  * `[:vfs, :read_file, :start | :stop | :exception]`
  * `[:vfs, :stream_read, :start | :stop | :exception]`
  * `[:vfs, :write_file, :start | :stop | :exception]`
  * `[:vfs, :mkdir, :start | :stop | :exception]`
  * `[:vfs, :rm, :start | :stop | :exception]`
  * `[:vfs, :walk, :start | :stop]`         (terminal — emitted on enumeration end)
  * `[:vfs, :materialize, :start | :stop | :exception]`
  * `[:vfs, :cache, :hit | :miss]`          (emitted by lazy backends themselves)

Metadata always includes `%{impl: <impl module>}`. Errors land in the
`:stop` event metadata as `%{error: %VFS.Error{...}}`.

# `mount`

```elixir
@type mount() :: {VFS.Path.t(), VFS.Mountable.t()}
```

A `{mountpoint, backend}` pair. The backend is any `VFS.Mountable`.

# `t`

```elixir
@type t() :: %VFS{mounts: [mount()]}
```

# `assert_implemented!`

```elixir
@spec assert_implemented!(term()) :: :ok
```

Raise a helpful error if `value` does not implement `VFS.Mountable`.

Useful at trust boundaries (constructor arguments, public API entry
points) where catching a missing `defimpl` early beats a
`Protocol.UndefinedError` deep in a downstream call.

## Examples

    iex> VFS.assert_implemented!(VFS.Memory.new())
    :ok

    iex> VFS.assert_implemented!(%URI{})
    ** (ArgumentError) %URI{} does not implement the VFS.Mountable protocol. Add `defimpl VFS.Mountable, for: URI do ... end` or pass a struct that has one.

# `capabilities`

```elixir
@spec capabilities(VFS.Mountable.t()) :: MapSet.t(VFS.Mountable.capability())
```

Return the capability set supported by `impl`.

## Examples

    iex> caps = VFS.capabilities(VFS.Memory.new())
    iex> MapSet.subset?(MapSet.new([:read, :write, :mkdir]), caps)
    true

# `exists?`

```elixir
@spec exists?(VFS.Mountable.t(), String.t()) :: {boolean(), VFS.Mountable.t()}
```

Return whether `path` exists, plus the possibly cache-updated state.

## Examples

    iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
    iex> {true, _mem} = VFS.exists?(mem, "/note.txt")
    iex> :ok
    :ok

# `materialize`

```elixir
@spec materialize(
  VFS.Mountable.t(),
  keyword()
) :: {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Pre-warm any internal cache. See `VFS.Mountable.materialize/2`.

Non-lazy backends usually return unchanged state.

## Examples

    iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
    iex> {:ok, ^mem} = VFS.materialize(mem)
    iex> :ok
    :ok

# `mkdir`

```elixir
@spec mkdir(VFS.Mountable.t(), String.t(), keyword()) ::
  {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Create directory at `path`.

Pass `parents: true` for `mkdir -p` behavior.

## Examples

    iex> mem = VFS.Memory.new()
    iex> {:ok, mem} = VFS.mkdir(mem, "/a/b", parents: true)
    iex> {true, _mem} = VFS.exists?(mem, "/a/b")
    iex> :ok
    :ok

# `mount`

```elixir
@spec mount(t(), String.t(), VFS.Mountable.t()) :: t()
```

Mount `backend` at `mountpoint`. If a mount already exists at the same
point, it is replaced. Mounts are kept sorted by mountpoint length
(longest first) so longest-prefix routing is a linear scan.

Raises `ArgumentError` if `backend` does not implement `VFS.Mountable`.

## Examples

    iex> fs = VFS.new() |> VFS.mount("/repo", VFS.Memory.new(%{"/x" => "1"}))
    iex> {:ok, "1", _fs} = VFS.read_file(fs, "/repo/x")
    iex> :ok
    :ok

# `mounts`

```elixir
@spec mounts(t()) :: [mount()]
```

Return the list of `{mountpoint, backend}` pairs in longest-first order.

## Examples

    iex> fs = VFS.new() |> VFS.mount("/repo", VFS.Memory.new()) |> VFS.mount("/", VFS.Memory.new())
    iex> fs |> VFS.mounts() |> Enum.map(&elem(&1, 0))
    ["/repo", "/"]

# `new`

```elixir
@spec new() :: t()
```

Build an empty mount table. Use `mount/3` to attach backends.

## Examples

    iex> %VFS{mounts: []} = VFS.new()

    iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/foo" => "bar"}))
    iex> {:ok, "bar", _fs} = VFS.read_file(fs, "/foo")
    iex> :ok
    :ok

# `read_file`

```elixir
@spec read_file(VFS.Mountable.t(), String.t()) ::
  {:ok, binary(), VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Read the entire content of `path`.

Derived from `stream_read/3` — runs the chunk stream into a binary. Use
`stream_read/3` directly when you need `:chunk_size`, `:byte_range`, or
`:line_range` options.

## Examples

    iex> mem = VFS.Memory.new(%{"/hello.txt" => "hello"})
    iex> {:ok, "hello", _mem} = VFS.read_file(mem, "/hello.txt")
    iex> :ok
    :ok

# `readdir`

```elixir
@spec readdir(VFS.Mountable.t(), String.t()) ::
  {:ok, Enumerable.t(String.t()), VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

List entries directly under directory `path`.

Returns an `t:Enumerable.t/0` of names: a list for bounded backends,
a `Stream` for paginated or unbounded ones. Treat it as an Enumerable
— `Enum.to_list/1` only when you know the listing is bounded,
`Stream.take/2` when you don't. See `VFS.Mountable.readdir/2`.

# `rm`

```elixir
@spec rm(VFS.Mountable.t(), String.t(), keyword()) ::
  {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Remove `path`.

Pass `recursive: true` to remove a directory tree.

## Examples

    iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
    iex> {:ok, mem} = VFS.rm(mem, "/note.txt")
    iex> {false, _mem} = VFS.exists?(mem, "/note.txt")
    iex> :ok
    :ok

# `stat`

```elixir
@spec stat(VFS.Mountable.t(), String.t()) ::
  {:ok, VFS.Stat.t(), VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Return metadata for `path`.

## Examples

    iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
    iex> {:ok, %VFS.Stat{type: :regular, size: 2}, _mem} = VFS.stat(mem, "/note.txt")
    iex> :ok
    :ok

# `stream_read`

```elixir
@spec stream_read(VFS.Mountable.t(), String.t(), keyword()) ::
  {:ok, Enumerable.t(binary()), VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Open `path` for streaming read. See `VFS.Mountable.stream_read/3`.

## Examples

    iex> mem = VFS.Memory.new(%{"/hello.txt" => "hello\nworld\n"})
    iex> {:ok, stream, _mem} = VFS.stream_read(mem, "/hello.txt", line_range: {2, 2})
    iex> Enum.to_list(stream)
    ["world"]

# `umount`

```elixir
@spec umount(t(), String.t()) :: t()
```

Remove the mount at `mountpoint`. No-op if no such mount.

## Examples

    iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/a" => "b"})) |> VFS.umount("/")
    iex> %VFS{mounts: []} = fs

# `walk`

```elixir
@spec walk(VFS.Mountable.t(), String.t(), keyword()) ::
  Enumerable.t({String.t(), VFS.Stat.t()})
```

Lazily walk the tree under `root`. Emits `{path, %VFS.Stat{}}` tuples.
Telemetry: `[:vfs, :walk, :start | :stop]` (terminal — emitted on
enumeration completion).

## Examples

    iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/a" => "1", "/b/c" => "2"}))
    iex> fs |> VFS.walk("/", []) |> Enum.map(&elem(&1, 0)) |> Enum.sort()
    ["/a", "/b/c"]

# `write_file`

```elixir
@spec write_file(VFS.Mountable.t(), String.t(), binary(), keyword()) ::
  {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
```

Write `content` to `path`.

Returns the updated filesystem state; thread it into subsequent calls.

## Examples

    iex> mem = VFS.Memory.new()
    iex> {:ok, mem} = VFS.write_file(mem, "/note.txt", "hi")
    iex> {:ok, "hi", _mem} = VFS.read_file(mem, "/note.txt")
    iex> :ok
    :ok

---

*Consult [api-reference.md](api-reference.md) for complete listing*
