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
- Host: GitHub
- URL: https://github.com/attackgoat/read-only
- Owner: attackgoat
- License: apache-2.0
- Created: 2026-05-23T10:43:05.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-23T18:33:26.000Z (about 1 month ago)
- Last Synced: 2026-07-03T22:34:25.038Z (2 days ago)
- Language: Rust
- Size: 23.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE-APACHE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# read-only
[](https://crates.io/crates/read-only)
[](https://docs.rs/read-only)
[](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.