Skip to content

Commit

Permalink
Derived type generics (#10)
Browse files Browse the repository at this point in the history
* Changed type transition steps in impls

* Added derived type generics support

* Split integration tests

* Added CI jobs

* Added .rustfmt.toml

* Updated docs

* Bump to 1.0.0 release
  • Loading branch information
bobozaur authored May 2, 2024
1 parent 1bd178f commit c591bf1
Show file tree
Hide file tree
Showing 27 changed files with 953 additions and 512 deletions.
76 changes: 76 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# yaml-language-server: $schema=./main.yaml
name: CI

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

env:
RUST_TOOLCHAIN: 1.74.0

jobs:
format:
name: Format
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3

- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: rustfmt

- uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check

clippy:
name: Clippy
needs: format
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3

- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: clippy

- uses: Swatinem/rust-cache@v2

- uses: actions-rs/cargo@v1
with:
command: clippy
args: --tests
env:
RUSTFLAGS: -D warnings


tests:
name: Tests
needs: clippy
runs-on: ubuntu-20.04

steps:
- uses: actions/checkout@v3

- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true

- uses: Swatinem/rust-cache@v2

- name: Run tests
run: cargo test
14 changes: 14 additions & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# rust-analyzer can be configured with nightly rustfmt
# i.e. for vscode in settings.json:
# "rust-analyzer.rustfmt.extraArgs": [
# "+nightly"
# ],
unstable_features = true
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
format_code_in_doc_comments = true
format_macro_bodies = true
format_macro_matchers = true
format_strings = true
comment_width = 100
wrap_comments = true
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.0.0] - 2024-05-02

### Added
- [#6](https://github.com/bobozaur/transitive/issues/6): Added generics support on the derived type.

### Changed

- Updated dependencies.
- Removed usage of `darling`.
- Improved path direction parsing.

## [0.5.0] - 2023-07-03

Expand Down
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "transitive"
version = "0.5.0"
version = "1.0.0"
edition = "2021"
authors = ["bobozaur"]
description = "Transitive derive macros for Rust."
Expand All @@ -16,7 +16,6 @@ proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
proc-macro2 = "1.0.63"
quote = "1.0.29"
syn = "2.0.23"
darling = "0.20.1"
proc-macro2 = "1.0.81"
quote = "1.0.36"
syn = "2.0.60"
62 changes: 30 additions & 32 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
//! This crate provides derive macros that take care of boilerplate code to make
//! transitive conversions in Rust using [`From`] and [`TryFrom`] traits.
//! This crate provides a derive macro that takes care of boilerplate code to make transitive
//! conversions in Rust using [`From`] and [`TryFrom`] traits.
//!
//! It's not magic and it is completely static. The derives here merely implement [`From`] or [`TryFrom`]
//! between types by relying on already defined impls between types provided in the path.
//! It's not magic and it's completely static. The derive here merely implements [`From`] or
//! [`TryFrom`] between types by relying on existent impls between items in the path.
//!
//! The path taken for transitions must be annotated (correctly) for transitions to work.
//! Additonally, there can only be one transition between a source and a target type,
//! as otherwise there would be duplicate trait implementations.
//! Additonally, there can only be one transition between a source and a target type, as otherwise
//! there would be duplicate trait implementations.
//!
//! The path is provided in the [`#[transitive]`] attribute along with a direction. Assuming we derive this on type `X`:
//! - `#[transitive(from(A, B, C))]` results in: A -> B -> C -> X
//! - `#[transitive(into(A, B, C))]` results in: X -> A -> B -> C
//! - `#[transitive(try_from(A, B, C))]` results in the fallible version of: A -> B -> C -> X
//! - `#[transitive(try_into(A, B, C))]` results in the fallible version of: X -> A -> B -> C
//! The path is provided in the [`#[transitive]`] attribute along with a direction:
//!
//! By default, for fallible conversions the error types must be convertible to the error type of the last conversion taking place.
//! So, in a `#[transitive(try_from(A, B, C))]` annotation on type `X`, the last conversion taking place is `C -> X`.
//! Let's call this `ErrC_X`. All error types resulting from the previous conversions must implement `From<Err> for ErrC_X`.
//! ```ignore, compile_fail
//! #[derive(Transitive)]
//! #[transitive(from(D, C, B))] // Results in `impl From<D> for A` as `D -> C -> B -> A`
//! #[transitive(into(B, C, D))] // Results in `impl From<A> for D` as `A -> B -> C -> D`
//! struct A;
//! ```
//!
//! # Conversions table:
//!
Expand All @@ -25,19 +24,18 @@
//! | A | #[transitive(into(B, C, D))] | `From<A> for D` | `From<A> for B`; `From<B> for C`; `From<C> for D` |
//! | A | #[transitive(from(D, C, B))] | `From<D> for A` | `From<D> for C`; `From<C> for B`; `From<B> for A` |
//! | A | #[transitive(try_into(B, C, D))] | `TryFrom<A> for D` | `TryFrom<A> for B`; `TryFrom<B> for C`; `TryFrom<C> for D`; errors must impl `From<ErrType> for <D as TryFrom<C>>::Error` |
//! | A | #[transitive(try_from(D, C, B))] | `TryFrom<D> for A` | `TryFrom<D> for C`; `TryFrom<C>` for B; `TryFrom<B> for A`; errors must `impl From<ErrType> for <A as TryFrom<B>>::Error` |
//! | A | #[transitive(try_from(D, C, B))] | `TryFrom<D> for A` | `TryFrom<D> for C`; `TryFrom<C> for B`; `TryFrom<B> for A`; errors must impl `From<ErrType> for <A as TryFrom<B>>::Error` |
//!
//!
//! # Custom error type:
//!
//! For `try_from` and `try_into` annotations, the macro attribute can accept an `error = "MyError"` argument,
//! like so: `#[transitive(try_into(A, B, C), error = "MyError")]`. This overrides the default behavior and allows
//! specifying a custom error type, but all the error types resulting from conversions must be convertible to this type.
//! For `try_from` and `try_into` annotations, the macro attribute can accept an `error = MyError`
//! argument as the last element, like so: `#[transitive(try_into(A, B, C, error = MyError))]`. This
//! overrides the default behavior and allows specifying a custom error type, but all the error
//! types resulting from conversions must be convertible to this type.
//!
//! # Examples:
//!
//! Assume you have types `A`, `B`, `C` and `D`:
//!
//! ```
//! use transitive::Transitive;
//!
Expand Down Expand Up @@ -107,8 +105,6 @@
//! C::from(A); // does not compile
//! ```
//!
//! The derive supports multiple `transitive` attribute instances, each providing a list of types as a path:
//!
//! ```
//! use transitive::Transitive;
//!
Expand Down Expand Up @@ -145,7 +141,8 @@
//! nature of the `from` and `try_from` attribute modifiers and the error transitions constraints:
//!
//! ```
//! use transitive::{Transitive};
//! #![allow(non_camel_case_types)]
//! use transitive::Transitive;
//!
//! // Note how the annotation now considers `A` as target type
//! // and `D`, the first element in the type list, as source.
Expand All @@ -154,7 +151,7 @@
//! struct A;
//!
//! #[derive(Transitive)]
//! #[transitive(try_from(D, C), error = "ConvErr")] // impl TryFrom<D> for B, with custom error
//! #[transitive(try_from(D, C, error = ConvErr))] // impl TryFrom<D> for B, with custom error
//! struct B;
//! struct C;
//! struct D;
Expand Down Expand Up @@ -196,23 +193,23 @@
//! type Error = ErrD_C;
//!
//! fn try_from(val: D) -> Result<Self, Self::Error> {
//! Ok(Self)
//! Ok(Self)
//! }
//! };
//!
//! impl TryFrom<C> for B {
//! type Error = ErrC_B;
//!
//! fn try_from(val: C) -> Result<Self, Self::Error> {
//! Ok(Self)
//! Ok(Self)
//! }
//! };
//!
//! impl TryFrom<B> for A {
//! type Error = ErrB_A;
//!
//! fn try_from(val: B) -> Result<Self, Self::Error> {
//! Ok(Self)
//! Ok(Self)
//! }
//! };
//!
Expand All @@ -223,14 +220,15 @@
mod transitive;

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Error};
use quote::ToTokens;
use syn::parse_macro_input;

use crate::transitive::TransitiveInput;

/// Derive macro that implements [From] for infallible transitions.
#[proc_macro_derive(Transitive, attributes(transitive))]
pub fn transitive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

transitive::transitive_impl(input)
.unwrap_or_else(Error::into_compile_error)
parse_macro_input!(input as TransitiveInput)
.to_token_stream()
.into()
}
31 changes: 0 additions & 31 deletions src/transitive/attr/idented.rs

This file was deleted.

28 changes: 0 additions & 28 deletions src/transitive/attr/mod.rs

This file was deleted.

52 changes: 50 additions & 2 deletions src/transitive/fallible/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
mod try_from;
mod try_into;

pub use try_from::TransitiveTryFrom;
pub use try_into::TransitiveTryInto;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
Error as SynError, Expr, Meta, Path, Result as SynResult, Token,
};
pub use try_from::TryTransitionFrom;
pub use try_into::TryTransitionInto;

/// A path list that may contain a custom error type.
pub struct FalliblePathList {
path_list: Vec<Path>,
error: Option<Path>,
}

impl Parse for FalliblePathList {
fn parse(input: ParseStream) -> SynResult<Self> {
let meta_list = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;

let mut path_list = Vec::with_capacity(meta_list.len());
let mut error = None;

let mut iter = meta_list.into_iter().peekable();

while let Some(meta) = iter.next() {
// If this is not the last element then it's definitely part of the path list
if iter.peek().is_some() {
match meta {
Meta::Path(path) => path_list.push(path),
_ => return Err(SynError::new(meta.span(), "invalid value in path list")),
};
} else {
// We reached the last element which could be the custom error type
match meta {
// Just a regular type path in the conversion path
Meta::Path(p) => path_list.push(p),
// Custom error, but must check that it's a type path
Meta::NameValue(n) if n.path.is_ident("error") => match n.value {
Expr::Path(p) => error = Some(p.path),
_ => return Err(SynError::new(n.value.span(), "error must be a type")),
},
_ => return Err(SynError::new(meta.span(), "invalid value in path list")),
}
}
}

let output = Self { path_list, error };
Ok(output)
}
}
Loading

0 comments on commit c591bf1

Please sign in to comment.