Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 125 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# substruct

![[crates.io](https://crates.io/crate/substruct)](https://img.shields.io/crates/v/substruct)
![[license](https://crates.io/crate/substruct)](https://img.shields.io/crates/l/substruct)
![[docs.rs](https://docs.rs/substruct)](https://img.shields.io/docsrs/substruct)
Expand All @@ -8,6 +9,7 @@ Substruct is a proc-macro wich allows you to easily declare strucs which are
subsets of another struct.

## Simple Example

A basic use of substruct looks like this

```rust
Expand All @@ -27,6 +29,7 @@ pub struct QueryParams {
```

which expands out to produce

```rust
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueryParams {
Expand All @@ -43,6 +46,7 @@ pub struct LimitedQueryParams {
```

## Complex Example

Substruct also supports copying attributes or adding attributes specific to a
subset of the child structs.

Expand Down Expand Up @@ -85,11 +89,130 @@ pub struct QueryParams {
}
```

## Field Type Transformations

Sometimes you may want a substruct to have a different type for a field than
the parent struct. For example, you might want an optional field in the parent
to be required in the substruct. This can be achieved by specifying transformations
inline with the struct name:

```rust
use substruct::substruct;

#[substruct(RequiredParams)]
#[derive(Clone, Debug)]
pub struct OptionalParams {
#[substruct(RequiredParams(unwrap))]
pub name: Option<String>,

#[substruct(RequiredParams)]
pub limit: usize,

pub description: Option<String>,
}
```

This generates:

```rust,ignore
#[derive(Clone, Debug)]
pub struct RequiredParams {
pub name: String, // Note: no longer Option<String>
pub limit: usize,
}

// Error type for failed conversions
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OptionalParamsConversionError {
MissingRequiredField(&'static str),
}

// TryFrom implementation instead of From
impl TryFrom<OptionalParams> for RequiredParams {
type Error = OptionalParamsConversionError;

fn try_from(value: OptionalParams) -> Result<Self, Self::Error> {
// Implementation that unwraps Option fields
}
}

// Method to convert back to parent struct
impl RequiredParams {
pub fn into_optional_params(self, description: Option<String>) -> OptionalParams {
// Implementation
}
}
```

Another example using `try_into` to convert between numeric types:

```rust
use substruct::substruct;

#[substruct(ConvertedParams)]
#[derive(Clone, Debug)]
pub struct OriginalParams {
#[substruct(ConvertedParams(try_into = u64))]
pub id: u32,

#[substruct(ConvertedParams)]
pub limit: usize,

pub description: Option<String>,
}
```

This generates:

```rust,ignore
#[derive(Clone, Debug)]
pub struct ConvertedParams {
pub id: u64, // Note: converted from u32 to u64
pub limit: usize,
}

// TryFrom implementation for type conversion
impl TryFrom<OriginalParams> for ConvertedParams {
type Error = OriginalParamsConversionError;

fn try_from(value: OriginalParams) -> Result<Self, Self::Error> {
Ok(Self {
id: value.id.try_into().map_err(|_|
OriginalParamsConversionError::ConversionFailed("id"))?,
limit: value.limit,
})
}
}

// Method to convert back using reverse conversion
impl ConvertedParams {
pub fn into_original_params(self, description: Option<String>) -> OriginalParams {
OriginalParams {
id: self.id.try_into().expect("reverse conversion should not fail"),
limit: self.limit,
description,
}
}
}
```

When field transformations are used:

- A `TryFrom<ParentStruct>` implementation is generated instead of `From`
- A conversion error type `{ParentStruct}ConversionError` is created with variants for different failure types
- An `into_{parent_struct}` method converts back to the parent

Supported transformations are specified inline with the struct name:

- `StructName(unwrap)`: Transforms `Option<T>` to `T` in the substruct
- `StructName(try_into = TargetType)`: Transforms the field type using `TryInto<TargetType>`

## Limitations

Substruct supports generics but will fail if the generic parameters are not
used by all of the child structs.


# See Also

- The [subenum](https://crates.io/crates/subenum) crate offers the same thing
for enums.
for enums.
157 changes: 134 additions & 23 deletions src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;

#[derive(Clone)]
pub(crate) enum Expr {
Ident(syn::Ident),
IdentWithTransform(IdentWithTransformExpr),
Not(NotExpr),
All(AllExpr),
Any(AnyExpr),
Expand All @@ -14,29 +16,44 @@ impl Expr {
pub fn evaluate(&self, ident: &syn::Ident) -> bool {
match self {
Self::Ident(lit) => ident == lit,
Self::IdentWithTransform(e) => e.evaluate(ident),
Self::Not(e) => e.evaluate(ident),
Self::Any(e) => e.evaluate(ident),
Self::All(e) => e.evaluate(ident),
}
}

pub fn get_transform(&self, ident: &syn::Ident) -> Option<&TransformType> {
match self {
Self::Ident(_) => None,
Self::IdentWithTransform(e) => e.get_transform(ident),
Self::Not(e) => e.get_transform(ident),
Self::Any(e) => e.get_transform(ident),
Self::All(e) => e.get_transform(ident),
}
}
}

impl Parse for Expr {
fn parse(input: ParseStream) -> syn::Result<Self> {
if !input.peek2(syn::token::Paren) {
return Ok(Self::Ident(input.parse()?));
}

let ident: syn::Ident = input.fork().parse()?;

match () {
_ if ident == "not" => input.parse().map(Self::Not),
_ if ident == "any" => input.parse().map(Self::Any),
_ if ident == "all" => input.parse().map(Self::All),
_ => Err(syn::Error::new(
ident.span(),
format!("unexpected operator `{ident}`, expected `not`, `any`, or `all`"),
)),
// If an identifier is followed by parentheses then it may be an operator
// like `not(...)` / `any(...)` / `all(...)` or a transform spec like
// `Name(unwrap)` / `Name(try_into = Type)`. Inspect without consuming
// by using a fork and then dispatch accordingly.
if input.peek(syn::Ident) && input.peek2(syn::token::Paren) {
let ident: syn::Ident = input.fork().parse()?;
match ident.to_string().as_str() {
"not" => input.parse().map(Self::Not),
"any" => input.parse().map(Self::Any),
"all" => input.parse().map(Self::All),
_ => {
// Parse as IdentWithTransform for struct names with transforms like A(unwrap)
input.parse().map(Self::IdentWithTransform)
}
}
} else {
// Plain identifier (no parentheses following) is just an Ident
Ok(Self::Ident(input.parse()?))
}
}
}
Expand All @@ -45,23 +62,107 @@ impl ToTokens for Expr {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Ident(ident) => ident.to_tokens(tokens),
Self::IdentWithTransform(e) => e.to_tokens(tokens),
Self::Not(e) => e.to_tokens(tokens),
Self::All(e) => e.to_tokens(tokens),
Self::Any(e) => e.to_tokens(tokens),
}
}
}

#[derive(Clone)]
pub(crate) struct IdentWithTransformExpr {
pub(crate) ident: syn::Ident,
pub(crate) paren: syn::token::Paren,
pub(crate) transform: TransformType,
}

#[derive(Clone)]
pub(crate) enum TransformType {
Unwrap,
TryInto(Box<syn::Type>),
}

impl IdentWithTransformExpr {
pub fn evaluate(&self, ident: &syn::Ident) -> bool {
self.ident == *ident
}

pub fn get_transform(&self, ident: &syn::Ident) -> Option<&TransformType> {
if self.ident == *ident {
Some(&self.transform)
} else {
None
}
}
}

impl Parse for IdentWithTransformExpr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let ident = input.parse()?;
let paren = syn::parenthesized!(content in input);

let transform = if content.peek(syn::Ident) {
let transform_ident: syn::Ident = content.parse()?;

if transform_ident == "unwrap" {
TransformType::Unwrap
} else if transform_ident == "try_into" {
// Parse "try_into = Type"
content.parse::<syn::Token![=]>()?;
let target_type: syn::Type = content.parse()?;
TransformType::TryInto(Box::new(target_type))
} else {
return Err(syn::Error::new_spanned(
&transform_ident,
"unknown transformation type, expected 'unwrap' or 'try_into'",
));
}
} else {
return Err(syn::Error::new(
content.span(),
"expected transformation type ('unwrap' or 'try_into = Type')",
));
};

Ok(Self {
ident,
paren,
transform,
})
}
}

impl ToTokens for IdentWithTransformExpr {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.ident.to_tokens(tokens);
self.paren.surround(tokens, |tokens| match &self.transform {
TransformType::Unwrap => {
tokens.extend(quote::quote! { unwrap });
}
TransformType::TryInto(ty) => {
tokens.extend(quote::quote! { try_into = #ty });
}
});
}
}

#[derive(Clone)]
pub(crate) struct NotExpr {
pub ident: syn::Ident,
pub paren: syn::token::Paren,
pub expr: Box<Expr>,
pub(crate) ident: syn::Ident,
pub(crate) paren: syn::token::Paren,
pub(crate) expr: Box<Expr>,
}

impl NotExpr {
pub fn evaluate(&self, ident: &syn::Ident) -> bool {
!self.expr.evaluate(ident)
}

pub fn get_transform(&self, ident: &syn::Ident) -> Option<&TransformType> {
self.expr.get_transform(ident)
}
}

impl Parse for NotExpr {
Expand All @@ -84,16 +185,21 @@ impl ToTokens for NotExpr {
}
}

#[derive(Clone)]
pub(crate) struct AnyExpr {
pub ident: syn::Ident,
pub paren: syn::token::Paren,
pub exprs: Punctuated<Expr, syn::Token![,]>,
pub(crate) ident: syn::Ident,
pub(crate) paren: syn::token::Paren,
pub(crate) exprs: Punctuated<Expr, syn::Token![,]>,
}

impl AnyExpr {
pub fn evaluate(&self, ident: &syn::Ident) -> bool {
self.exprs.iter().any(|e| e.evaluate(ident))
}

pub fn get_transform(&self, ident: &syn::Ident) -> Option<&TransformType> {
self.exprs.iter().find_map(|e| e.get_transform(ident))
}
}

impl Parse for AnyExpr {
Expand Down Expand Up @@ -124,16 +230,21 @@ impl ToTokens for AnyExpr {
}
}

#[derive(Clone)]
pub(crate) struct AllExpr {
pub ident: syn::Ident,
pub paren: syn::token::Paren,
pub exprs: Punctuated<Expr, syn::Token![,]>,
pub(crate) ident: syn::Ident,
pub(crate) paren: syn::token::Paren,
pub(crate) exprs: Punctuated<Expr, syn::Token![,]>,
}

impl AllExpr {
pub fn evaluate(&self, ident: &syn::Ident) -> bool {
self.exprs.iter().all(|e| e.evaluate(ident))
}

pub fn get_transform(&self, ident: &syn::Ident) -> Option<&TransformType> {
self.exprs.iter().find_map(|e| e.get_transform(ident))
}
}

impl Parse for AllExpr {
Expand Down
Loading
Loading