# read-only

<p align="center">
  <img src=".github/logo.svg" alt="read-only logo" width="600">
</p>

[![Crates.io](https://img.shields.io/crates/v/read-only.svg)](https://crates.io/crates/read-only)
[![docs.rs](https://docs.rs/read-only/badge.svg)](https://docs.rs/read-only)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT)

Read-only field exposure and safe composition helpers via proc macros.

This crate intentionally combines two patterns:

- `#[cast]` is the classic cast-based readonly-field pattern, re-exported from
  [`readonly::make`](https://docs.rs/readonly/latest/readonly/attr.make.html).
- `#[embed]` is this crate's safe composition-based alternative that builds a
  dedicated `ReadOnly*` view struct and dereferences into it without unsafe code.

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
read-only = "0.1"
```

## Usage

Two attribute macros are provided.

### `#[cast]`

> [!NOTE]
> `#[cast]` is a re-export of `readonly::make`. You can use either
> `read_only::cast` or `readonly::make` — they are the same proc macro.

Implements `Deref` via an unsafe pointer cast — wrapped safely so callers
never write `unsafe` themselves. Fields remain writable inside the defining
module but become read-only externally.

**All fields read-only** (no `#[readonly]` annotation needed):

```rust
use read_only::cast;

#[cast]
pub struct Config {
    pub timeout: u64,
    pub retries: u32,
}

// Inside this module:  config.timeout = 5;        // ✅ write
// Outside:             config.timeout = 5;        // ❌ compile error
//                      let t = config.timeout;    // ✅ read
```

**Selective read-only** (annotate specific fields with `#[readonly]`):

```rust
use read_only::cast;

#[cast]
pub struct Device {
    #[readonly]
    pub serial: u64,
    pub mutable_count: i32,
    internal: f64, // stays private in both views
}

// Inside this module:  dev.mutable_count = 1;     // ✅ write
//                      dev.serial = 2;             // ✅ write (inside module)
// Outside:             dev.mutable_count = 1;      // ✅ write (not readonly)
//                      let s = dev.serial;         // ✅ read only
//                      dev.serial = 2;             // ❌ compile error
```

When no `#[readonly]` attribute appears on any field, the entire struct
becomes read-only through a generated view struct. When any field is annotated,
only annotated fields are lifted into the read-only view; unannotated fields
keep their original mutability.

### `#[embed]`

Moves `#[readonly]` annotated fields into a `ReadOnly*` struct that is embedded
inside the original struct. A safe `Deref` implementation is generated — no
unsafe code is involved.

For `#[embed]`, `pub` on a `#[readonly]` field is optional: the generated
readonly view promotes inherited visibility to the struct's visibility.

```rust
use read_only::embed;

#[embed]
pub struct Device {
    #[readonly]
    pub handle: u64,
    #[readonly]
    pub label: String,
    pub mutable: i32,
}

impl Device {
    pub fn new(handle: u64, label: &str, mutable: i32) -> Self {
        Self {
            read_only: ReadOnlyDevice { handle, label: label.to_string() },
            mutable,
        }
    }
}

// let mut dev = Device::new(42, "dev1", 0);
// let h = dev.handle;    // ✅ read — goes through Deref to ReadOnlyDevice
// let l = &dev.label;    // ✅ read — goes through Deref
// dev.mutable = 1;       // ✅ write — mutable stays on the outer struct
// dev.handle = 0;        // ❌ compile error — handle is read-only
```

> [!NOTE]
> The generated `ReadOnly*` struct is public. You can implement `Deref`, `Display`,
> or other traits on it to forward or customize behavior.

Rustdoc uses a docs-only outer struct definition for `#[embed]` so readonly
fields appear directly in the rendered struct docs.

This behavior is controlled by the default-enabled `docs` feature. Tools or
workflows that do a stricter `cfg(doc)` compile can fail.

## Choosing a Macro

- Use `#[cast]` if you want the familiar `readonly` behavior and the smallest
  syntax change.
- Use `#[embed]` if you want an explicit readonly view type and a safe
  composition-based implementation with no unsafe code in the generated access
  path.

## Examples

Runnable examples are available under `examples/`:

- `cargo run --example basic`
- `cargo run --example embed_deref`

## Alternatives

Directly comparable:

- [`readonly`](https://crates.io/crates/readonly): the canonical cast-based
  readonly-field crate and the upstream implementation re-exported here as
  `#[cast]`.

Related accessor-generation crates:

- [`getset`](https://crates.io/crates/getset): generates getter/setter methods
  instead of preserving direct field reads.
- [`derive-getters`](https://crates.io/crates/derive-getters): generates public
  getter methods for named structs.
- [`fieldwork`](https://crates.io/crates/fieldwork): a broader accessor macro
  system for structs and enums with many generated method styles.

The main difference is that those crates expose access through generated
methods, while `read-only` focuses on preserving direct readonly field access
syntax.

## Testing

The workspace includes both compile-time macro tests and runtime tests.

- `trybuild` macro tests cover pass/fail macro expansion behavior.
- `tests/miri.rs` exercises the `#[cast]` unsafe pointer-cast path under
  Miri to help detect undefined behavior and provenance issues.

Run the Miri suite with:

```sh
cargo +nightly miri test --test miri
```

For source coverage, the CI allows one uncovered line/region for the remaining
LLVM coverage artifact in the proc-macro expansion code:

```sh
cargo llvm-cov --workspace --all-features --summary-only --fail-uncovered-lines 1 --fail-uncovered-regions 1
```

## License

Licensed under either of

- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT license ([LICENSE-MIT](LICENSE-MIT))

at your option.
