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

Pluggable virtual filesystem.

Implementations are plain structs; the protocol dispatches on the struct
type. Backend authors typically use `VFS.Skeleton` inside their `defimpl`
block to inherit defaults for `walk/3` and `materialize/2`, then override
whichever they want native impls for.

## Path contract

All paths reaching a backend are absolute, already normalized, with a
leading `/`. Backends behave as if rooted at `/`. Mount-prefix stripping
happens in `VFS`'s defimpl before the call reaches a leaf backend.

## State threading

*Every* operation — including reads — returns the (possibly updated)
impl as the last element of its success tuple. Lazy backends (e.g. an
exgit-backed mount with a partial-clone repo) cache fetched data in their
struct on read; throwing the updated struct away destroys those caches.
Callers thread the new state forward.

The one exception is `walk/3`, which returns a bare `t:Enumerable.t/0`.
Cache state populated during enumeration does not escape — call
`materialize/2` first if you need the cache primed before iteration.

## Errors

All errors are `%VFS.Error{}` exceptions. Pattern match on `:kind` for
flow control:

    case VFS.read_file(fs, path) do
      {:ok, bin, fs} -> ...
      {:error, %VFS.Error{kind: :enoent}} -> ...
      {:error, %VFS.Error{kind: :eisdir}} -> ...
    end

See `VFS.Error` for the full set of `:kind` values.

# `capability`

```elixir
@type capability() ::
  :read | :write | :mkdir | :native_walk | :native_stream_read | :lazy
```

Capability flags reported by `capabilities/1`.

  * `:read`   — `stream_read/3`, `stat/2`, `readdir/2`, `walk/3` work.
  * `:write`  — `write_file/4` and `rm/3` work on file paths. (Does NOT
                imply `mkdir/3`; flat-keyed backends like S3 / postgres
                support `:write` without `:mkdir`.)
  * `:mkdir`  — `mkdir/3` works (the backend models empty directories).
  * `:lazy`   — `materialize/2` does meaningful work (the backend has
                a cache that can be pre-warmed).
  * `:native_walk`        — `walk/3` is implemented natively (faster than
                            the default composed-from-stat-and-readdir).
  * `:native_stream_read` — `stream_read/3` is implemented natively.

# `path`

```elixir
@type path() :: String.t()
```

An absolute, normalized path (see `VFS.Path`).

# `t`

```elixir
@type t() :: struct()
```

Any struct that implements `VFS.Mountable`.

# `capabilities`

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

Return the set of capabilities this impl supports.

Capability claims should match observed behavior: for example, a backend
without `:mkdir` should consistently return `:erofs` or `:enotsup` from
`mkdir/3`.

# `exists?`

```elixir
@spec exists?(t(), path()) :: {boolean(), t()}
```

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

Implementations normalize `path`, check both files and directories, and
return the updated impl even for negative lookups so lazy backends can keep
metadata caches warm.

# `materialize`

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

Pre-warm any internal cache. No-op for non-lazy backends. Lazy backends
(e.g. partial-clone exgit repos) use this to avoid per-blob round-trips
during a subsequent bulk traversal.

# `mkdir`

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

Create a directory at `path`. Options:

  * `:parents` — create missing intermediate directories (`mkdir -p`)

# `readdir`

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

Return entries directly under directory `path` — names only, no leading
slash, no path separators within names.

Returns an `t:Enumerable.t/0` of strings. Backends with bounded
directories should return a list (the simplest Enumerable; `length/1`
and `Enum.sort/1` work natively on it). Backends with paginated or
unbounded listings (S3 with many keys, a database-backed store, a
virtual `/integers/N` namespace) should return a `Stream`. Consumers
should treat the result as an Enumerable: use `Enum.to_list/1` or
`Stream.take/2` as appropriate; do not assume `length/1` works.

Order: bounded backends should return entries in lexicographic order
by convention (matches POSIX, S3 ListObjects). Unbounded backends
document their order in the impl's moduledoc.

# `rm`

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

Remove `path`. Options:

  * `:recursive` — remove a directory and all contents (default `false`).
    Without `:recursive`, `rm` on a directory returns
    `{:error, %VFS.Error{kind: :eisdir}}`.

# `stat`

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

Return metadata for `path`.

Success returns `%VFS.Stat{}` and the updated impl. Failure returns a
structured `%VFS.Error{}`; callers pattern-match on `error.kind`.

# `stream_read`

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

Open `path` for streaming read. Returns an `t:Enumerable.t/0` that emits
binary chunks. Options:

  * `:chunk_size`  — default `64 * 1024`
  * `:byte_range`  — `{start, length}` — start is 0-based, returns up to
    `length` bytes starting at `start`
  * `:line_range`  — `{first, last}` — 1-based, inclusive line numbers;
    `last` may be `:end` to read to EOF

The returned impl reflects state needed to *open* the stream; cache state
populated during enumeration does not escape (see `walk/3` caveat).

# `walk`

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

Lazily walk the tree under `root`. Returns an `t:Enumerable.t/0` that emits
`{path, %VFS.Stat{}}` tuples. Options:

  * `:max_depth`        — `:infinity` (default) or non-neg integer
  * `:include_dirs`     — emit directory entries themselves (default `false`)

Returns a bare `t:Enumerable.t/0`, not a `{:ok, _, t}` tuple. Cache state
populated during enumeration does not escape; call `materialize/2` first
for agent loops that re-touch files after a walk.

Default-implementation traversal is depth-first and **fully lazy**:
`Stream.take/2` halts the traversal as soon as the consumer has enough,
including over backends whose `readdir/2` returns an unbounded
`t:Enumerable.t/0` (paginated S3 listings, virtual `/integers/N`-style
namespaces, database cursors). The default walker pulls one entry at a
time from each directory's readdir output.

When the mount table contains overlapping mounts, walk respects
longest-prefix shadowing — paths reachable only through a shadowed
prefix are not yielded, matching `read_file/2`'s point-lookup behavior.

# `write_file`

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

Write `content` to `path`.

Success returns the updated impl. Read-only wrappers should return `:erofs`;
backends that fundamentally cannot write should return `:enotsup`.
Options are reserved for future use.

---

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