omitty 0.1.0

TypeScript-like Omit<> for Rust structs - no boilerplate
Documentation
//! # omitty
//!
//! A Rust procedural macro for generating type variants with omitted fields.
//!
//! ## Overview
//!
//! `omitty` provides a simple way to create variants of your structs with specific fields omitted,
//! similar to TypeScript's `Omit<>` utility type. This is useful for creating public-facing types
//! from internal types, API response types, or different permission-level views of your data.
//!
//! ## Features
//!
//! - Simple syntax - Generate variants with just `#[omit_variant(VariantName, field1, field2)]`
//! - Automatic derive - Generates Debug, Clone, Serialize, Deserialize, PartialEq, Eq
//! - Serde compatible - Works seamlessly with serde for JSON/serialization
//! - Type-safe - Compile-time guarantees for omitted fields
//!
//! ## Example
//!
//! ```rust
//! use omitty::OmittableType;
//!
//! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, OmittableType)]
//! #[omit_variant(UserPublic, secret_token)]
//! pub struct User {
//!     pub id: String,
//!     pub username: String,
//!     pub secret_token: String,
//! }
//!
//! // This generates:
//! // pub struct UserPublic {
//! //     pub id: String,
//! //     pub username: String,
//! // }
//! ```
//!
//! ## Multiple Omitted Fields
//!
//! ```rust
//! use omitty::OmittableType;
//!
//! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, OmittableType)]
//! #[omit_variant(UserPublic, password, email, phone)]
//! pub struct User {
//!     pub id: String,
//!     pub username: String,
//!     pub password: String,
//!     pub email: String,
//!     pub phone: String,
//! }
//! ```
//!
//! ## Multiple Variants
//!
//! ```rust
//! use omitty::OmittableType;
//!
//! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, OmittableType)]
//! #[omit_variant(UserPublic, password, email)]
//! #[omit_variant(UserMinimal, password, email, bio, avatar)]
//! pub struct User {
//!     pub id: String,
//!     pub username: String,
//!     pub password: String,
//!     pub email: String,
//!     pub bio: String,
//!     pub avatar: String,
//! }
//! ```

extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{format_ident, quote};
use syn::{Attribute, DeriveInput, Fields, Type, parse_macro_input, spanned::Spanned};

#[proc_macro_derive(Omittable, attributes(omit))]
pub fn derive_omittable(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let fields = match &input.data {
        syn::Data::Struct(s) => match &s.fields {
            Fields::Named(named) => &named.named,
            _ => {
                let err = syn::Error::new(
                    input.ident.span(),
                    "omitty: Omittable can only be derived for structs with named fields (not tuple structs or unit structs)",
                );
                return err.to_compile_error().into();
            }
        },
        _ => {
            let err = syn::Error::new(
                input.ident.span(),
                "omitty: Omittable can only be derived for structs (not enums or unions)",
            );
            return err.to_compile_error().into();
        }
    };

    for field in fields {
        if let Some(omit_attr) = field.attrs.iter().find(|a| path_is(a, "omit")) {
            let (omit_list, _explicit_ident) = match parse_omit_attr(omit_attr) {
                Ok(result) => result,
                Err(err) => return err.to_compile_error().into(),
            };

            // Typ bazowy pola
            let base_ident = match &field.ty {
                Type::Path(tp) if tp.qself.is_none() => tp
                    .path
                    .segments
                    .last()
                    .map(|seg| seg.ident.clone())
                    .unwrap_or_else(|| Ident::new("Unknown", Span::call_site())),
                _ => {
                    let err = syn::Error::new(
                        field.ty.span(),
                        "omitty: field type must be a path type (e.g., User, MyType). Complex types like references, arrays, or tuples are not supported",
                    );
                    return err.to_compile_error().into();
                }
            };

            let err = syn::Error::new(
                field.span(),
                format!(
                    "omitty: Cannot automatically generate omitted type for this field.\n\
                     Solution: Add #[derive(OmittableType)] to the base type '{}' or manually define the omitted type variant.",
                    base_ident
                ),
            );
            return err.to_compile_error().into();
        }
    }

    TokenStream::new()
}

#[proc_macro_derive(OmittableType, attributes(omit_variant))]
pub fn derive_omittable_type(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let fields = match &input.data {
        syn::Data::Struct(s) => match &s.fields {
            Fields::Named(named) => &named.named,
            _ => return TokenStream::new(),
        },
        _ => return TokenStream::new(),
    };

    let mut variants = Vec::<proc_macro2::TokenStream>::new();

    for attr in &input.attrs {
        if path_is(attr, "omit_variant") {
            match parse_variant_attr(attr) {
                Ok((variant_name, omit_list)) => {
                    let kept_fields = fields
                        .iter()
                        .filter(|f| {
                            let fname = f.ident.as_ref().unwrap().to_string();
                            !omit_list.iter().any(|x| x == &fname)
                        })
                        .collect::<Vec<_>>();

                    let vis = &input.vis;
                    let variant_ident = format_ident!("{}", variant_name);

                    variants.push(quote! {
                        #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
                        #vis struct #variant_ident {
                            #(#kept_fields),*
                        }
                    });
                }
                Err(err) => return err.to_compile_error().into(),
            }
        }
    }

    quote! {
        #(#variants)*
    }
    .into()
}

fn path_is(a: &Attribute, name: &str) -> bool {
    a.path().is_ident(name)
}

fn parse_omit_attr(attr: &Attribute) -> syn::Result<(Vec<String>, Option<String>)> {
    let mut fields = Vec::<String>::new();
    let mut explicit_ident: Option<String> = None;

    attr.parse_nested_meta(|meta| {
        if meta.path.is_ident("ident") {
            let value = meta.value()?;
            let s: syn::LitStr = value.parse()?;
            explicit_ident = Some(s.value());
            return Ok(());
        }

        if meta.path.is_ident("fields") {
            meta.parse_nested_meta(|nested| {
                let field_name = meta
                    .path
                    .get_ident()
                    .ok_or_else(|| nested.error("expected field name identifier"))?
                    .to_string();
                fields.push(field_name);
                Ok(())
            })?;
            return Ok(());
        }

        let field_name = meta
            .path
            .get_ident()
            .ok_or_else(|| meta.error("expected field name identifier"))?
            .to_string();
        fields.push(field_name);
        Ok(())
    })?;

    Ok((fields, explicit_ident))
}

fn parse_variant_attr(attr: &Attribute) -> syn::Result<(String, Vec<String>)> {
    let mut name: Option<String> = None;
    let mut fields = Vec::<String>::new();

    attr.parse_nested_meta(|meta| {
        // First argument is the variant name
        if name.is_none() {
            let variant_name = meta
                .path
                .get_ident()
                .ok_or_else(|| meta.error("expected variant name as first argument"))?
                .to_string();
            name = Some(variant_name);
            return Ok(());
        }

        // All subsequent arguments are fields to omit
        let field_name = meta
            .path
            .get_ident()
            .ok_or_else(|| meta.error("expected field name identifier"))?
            .to_string();
        fields.push(field_name);
        Ok(())
    })?;

    let name = name.ok_or_else(|| {
        syn::Error::new_spanned(
            attr,
            "missing variant name as first argument in omit_variant attribute. Usage: #[omit_variant(VariantName, field1, field2, ...)]",
        )
    })?;

    Ok((name, fields))
}