Skip to content

Commit

Permalink
More docs yay
Browse files Browse the repository at this point in the history
  • Loading branch information
fasterthanlime committed Dec 5, 2024
1 parent 951552f commit 8cf9e27
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 17 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ But it's a lot of going back and forth and adapting the impl to the trait or the
A block like this contains everything you need to declare the corresponding trait:

```rust
#[con::spec]
#[con::export]
impl RequestBuilder for RequestBuilderImpl {
fn add_header(&mut self, name: &str, value: &str) -> &mut Self {
self.headers.push((name.to_string(), value.to_string()));
Expand Down Expand Up @@ -55,7 +55,7 @@ it would slow down its build time significantly, bringing in all sorts of depend
So instead, although `con` itself _is_ a proc-macro crate, it has zero dependencies, and
doesn't transform the token stream at all.

It merely defines attributes like `[con::spec]`, that a separate tool, `con-cli`, will look for,
It merely defines attributes like `[con::export]`, that a separate tool, `con-cli`, will look for,
to know which trait definitions to generate.

In short, running `con` in a workspace will:
Expand Down
2 changes: 1 addition & 1 deletion con-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# con-cli

`con-cli` generates the consumer crates corresponding to module implementation crates marked with `#[con::spec]` attributes. This tool scans the workspace for crates starting with `mod-` and generates corresponding `con-` crates that contain just the trait definitions and public interfaces.
`con-cli` generates the consumer crates corresponding to module implementation crates marked with `#[con::export]` attributes. This tool scans the workspace for crates starting with `mod-` and generates corresponding `con-` crates that contain just the trait definitions and public interfaces.

## Installation

Expand Down
8 changes: 4 additions & 4 deletions con-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Their crate name is `mod-<name>` — we want to generate crates named `con-<name>`, that have everything
* except anything under a `#[cfg(feature = "impl")]` section (which includes `struct ModImpl`, `impl Mod for ModImpl`, etc.)
*
* The key change is looking for `#[con::spec]` attributes on impl blocks. When we find these:
* The key change is looking for `#[con::export]` attributes on impl blocks. When we find these:
* 1. Generate corresponding trait definitions
* 2. Write them to src/.con/spec.rs with a machine-generated notice
* 3. Make sure there is an include statement for this file in the mod's lib.rs
Expand All @@ -13,7 +13,7 @@
* The generated spec.rs file should:
* - Have a clear machine-generated notice and regeneration instructions
* - Not be counted in mod timestamps since it's generated
* - Contain all trait definitions derived from #[con::spec] impls
* - Contain all trait definitions derived from #[con::export] impls
*
* The 'con' command-line utility will:
* 1. List all mods in `mods/`
Expand All @@ -23,7 +23,7 @@
* 2c. If force flag is passed, or con directory is missing, or mod timestamps are newer:
* 2c1. Parse mod's lib.rs with syn and:
* - Strip #[cfg(feature = "impl")] items
* - Find #[con::spec] impls and generate traits
* - Find #[con::export] impls and generate traits
* - Write traits to src/.con/spec.rs
* 2c2. Generate new `Cargo.toml` based on mod's `Cargo.toml`, updating name
* 2c3. Generate full tree for con version including copied spec.rs
Expand Down Expand Up @@ -439,7 +439,7 @@ fn transform_macro_items(items: &mut Vec<Item>, added_items: &mut Vec<Item>) {
for attr in &imp.attrs {
if attr.path().segments.len() == 2
&& attr.path().segments[0].ident == "con"
&& attr.path().segments[1].ident == "spec"
&& attr.path().segments[1].ident == "export"
{
let iface_typ = if let Ok(_meta) = attr.meta.require_path_only() {
Some(InterfaceType::Sync)
Expand Down
160 changes: 150 additions & 10 deletions con/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

# con

`con` provides attribute macros like `#[con::spec]` used to mark trait implementations that should have corresponding trait definitions generated in consumer crates.
`con` provides attribute macros like `#[con::export]` used to mark trait implementations that should have corresponding trait definitions generated in consumer crates.

This crate has zero dependencies and does not perform any token transformations - it simply provides the attribute definitions that the `con-cli` tool looks for when generating consumer crates.

## Usage

Slap `#[con::spec]` on impl blocks whose trait you want `con-cli` to genaerte:
Slap `#[con::export]` on impl blocks whose trait you want `con-cli` to genaerte:

```rust
#[con::spec]
#[con::export]
impl Mod for ModImpl {
/// Parses command line arguments
fn parse(&self) -> Args {
Expand All @@ -23,20 +23,20 @@ impl Mod for ModImpl {
```

> **Warning**
> Only dyn-compatible traits can be marked with `#[con::spec]` — dynamic dispatch
> Only dyn-compatible traits can be marked with `#[con::export]` — dynamic dispatch
> is kinda the whole point.
Traits generated by `con` are `Send + Sync + 'static` by default. If you need a trait to be
not sync, you can pass `nonsync` as an arugment to `con::spec`:
not sync, you can pass `nonsync` as an arugment to `con::export`:

```rust
#[con::spec]
#[con::export]
impl Foo for FooImpl {}

// will generate:
// trait Foo: Send + Sync + 'static { }

#[con::spec(nonsync)]
#[con::export(nonsync)]
impl Bar for BarImpl {}

// will generate:
Expand All @@ -55,7 +55,7 @@ because it must be able to be constructed dynamically when the mod is loaded, fr
If you need your initialization to take arguments, you can simply export two interfaces:

```rust
#[con::spec]
#[con::export]
impl Mod for ModImpl {
type Error = anyhow::Error;

Expand All @@ -69,7 +69,7 @@ impl Mod for ModImpl {
}
}

#[con::spec]
#[con::export]
impl Client for ClientImpl {
fn send_request(&self, request: Request) -> Result<Response, anyhow::Error> {
// ...
Expand All @@ -79,4 +79,144 @@ impl Client for ClientImpl {

## Limitations

con will expect all your exported traits to be
con will expect all your exported traits to be [`dyn`](https://doc.rust-lang.org/std/keyword.dyn.html)-compatible (this used to be call "object safe")

Here's a list of things you cannot do.

### You cannot have generic type parameters

Traits exported by con cannot have generic type parameters.

```rust
// ❌ This won't work
#[con::export]
impl Parser<T> for JsonParser<T> {
fn parse(&self, input: &str) -> Result<T>;
}
```

### You cannot have generic methods

Methods in exported traits cannot have generic type parameters. Use trait objects or concrete types instead:

```rust
// ❌ This won't work
#[con::export]
impl Parser for JsonParser {
fn parse<T: DeserializeOwned>(&self, input: &str) -> Result<T>;
}
```

### You cannot use `impl Trait`

Neither argument position nor return position `impl Trait` is supported — they're essentially
generic type parameters in disguise.

```rust
// ❌ This won't work
#[con::export]
impl Handler for MyHandler {
fn process(&self, transform: impl Fn(u32) -> u32) -> impl Iterator<Item = u32>;
}
```

### You can (and should) use boxed trait objects

A surprising amount of things can be achieved through boxed trait objects if most of your traits are dyn-compatible:

```rust
// that's okay
#[con::export]
impl Handler for MyHandler {
fn process(&self, transform: Box<dyn Fn(u32) -> u32>) -> Box<dyn Iterator<Item = u32>>;
}
```

Note that if you don't need ownership of something, you can just take a reference to it, like the transform function here:

```rust
// that's okay too!
#[con::export]
impl Handler for MyHandler {
fn process(&self, transform: &dyn Fn(u32) -> u32) -> Box<dyn Iterator<Item = u32>>;
}
```

### Async functions are not supported yet

async fns in trait (AFIT) are supported by Rust as of 1.75, but as of 1.83, they are still
not dyn-compatible, so this won't work:

```rust
// ❌ This won't work yet
#[con::export]
impl Client for HttpClient {
async fn fetch(&self, url: &str) -> Result<Response> {
reqwest::get(url).await
}
}
```

However, you can return boxed futures:

```rust
// no need to pull in `futures-core` for this
pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;

#[con::export]
impl Client for HttpClient {
fn fetch(&self, url: &str) -> BoxFuture<'_, Result<Response>> {
Box::pin(async move {
reqwest::get(url).await
})
}
}
```

Sometimes you'll need to be a bit more explicit with lifetimes:

```rust
#[con::export]
impl Client for HttpClient {
fn fetch<'fut>(&'fut self, url: &'fut str) -> BoxFuture<'fut, Result<Response>> {
Box::pin(async move {
reqwest::get(url).await
})
}
}
```

Support for `dyn-compatible` async fn in traits is in the cards afaict, see the
[dynosaur](https://crates.io/crates/dynosaur) crate for a peek into the future (however you
cannot use it with con).

### Self type restrictions

You cannot take or return `self` by value, but you can use `Box<Self>` or `Arc<Self>` receivers:

```rust
#[con::export]
impl Builder for RequestBuilder {
// ❌ These won't work: we don't know the size of "Self"
fn with_header(mut self, name: &str) -> Self {
// etc.
}
fn build(self) -> Request {
// etc.
}
}

#[con::export]
impl Builder for RequestBuilder {
// ✅ These will work: a Box<Self> is the size of a pointer
fn with_header(mut self: Box<Self>, name: &str) -> Box<Self> {
// etc.
}
fn build(self: Box<Self>) -> Request {
// etc.
}
}
```

Essentially, as a consumer, we don't know the size of "Self" — so we need the indirection.
References (`&self`, `&mut self`) are always fine.

0 comments on commit 8cf9e27

Please sign in to comment.