An open API service indexing awesome lists of open source software.

https://github.com/attackgoat/read-only

Read-only field exposure and safe composition helpers via proc macros
https://github.com/attackgoat/read-only

Last synced: 2 days ago
JSON representation

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

Awesome Lists containing this project

README

          

# read-only


read-only logo

[![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.

```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.

## 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.