From 76358fc961cbf131b1ee88409d8ae78da9d3c4be Mon Sep 17 00:00:00 2001 From: naskya Date: Fri, 12 Jul 2024 01:13:18 +0900 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 61 +++++++++++++++++++ Cargo.toml | 7 +++ optionalize-derive/Cargo.toml | 12 ++++ optionalize-derive/src/lib.rs | 106 +++++++++++++++++++++++++++++++++ optionalize-derive/src/util.rs | 100 +++++++++++++++++++++++++++++++ optionalize/Cargo.toml | 10 ++++ optionalize/src/lib.rs | 16 +++++ src/main.rs | 27 +++++++++ 9 files changed, 340 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 optionalize-derive/Cargo.toml create mode 100644 optionalize-derive/src/lib.rs create mode 100644 optionalize-derive/src/util.rs create mode 100644 optionalize/Cargo.toml create mode 100644 optionalize/src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4ca719d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "optionalize" +version = "0.1.0" +dependencies = [ + "optionalize-derive", +] + +[[package]] +name = "optionalize-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test" +version = "0.1.0" +dependencies = [ + "optionalize", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..27bdd1e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "test" +version = "0.1.0" +edition = "2021" + +[dependencies] +optionalize = { path = "optionalize" } diff --git a/optionalize-derive/Cargo.toml b/optionalize-derive/Cargo.toml new file mode 100644 index 0000000..34ec8ed --- /dev/null +++ b/optionalize-derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "optionalize-derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0.70", features = ["extra-traits"] } +quote = "1.0.36" +proc-macro2 = "1.0.86" diff --git a/optionalize-derive/src/lib.rs b/optionalize-derive/src/lib.rs new file mode 100644 index 0000000..260e166 --- /dev/null +++ b/optionalize-derive/src/lib.rs @@ -0,0 +1,106 @@ +mod util; + +use quote::quote; +use syn::{parse_macro_input, spanned::Spanned}; + +#[proc_macro_derive(Optionalize, attributes(opt))] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + derive_impl(parse_macro_input!(input)).into() +} + +fn derive_impl(input: syn::DeriveInput) -> proc_macro2::TokenStream { + let base = &input.ident; + let vis = &input.vis; + + let attrs = input + .attrs + .iter() + .filter_map(|attr| match util::parse_name_path_attr(attr, "opt") { + Some((name, path)) if name == "name" => Some(path), + _ => None, + }) + .collect::>(); + + if attrs.len() > 1 { + panic!("`opt` attribute is set more than once"); + } + + let opt_name = if attrs.is_empty() { + format!("{}Opt", base) + } else { + if attrs[0].segments.len() != 1 { + panic!("usage of `opt` attribute (the one at the top-level) is incorrect"); + } + attrs[0].segments[0].ident.to_string() + }; + + let opt = syn::Ident::new(&opt_name, input.span()); + + let fields = util::struct_fields_info(&input.data, "opt"); + + let opt_fields = fields.iter().map(|field| { + let name = field.ident; + let vis = field.vis; + + match field.is_nested { + false => { + let underlying_ty = field.underlying_ty; + quote! { #vis #name: ::std::option::Option<#underlying_ty> } + } + true => { + let nested_opt_ty = field.nested_opt_ty.as_ref().unwrap(); + quote! { #vis #name: ::std::option::Option<#nested_opt_ty> } + } + } + }); + + let or_fields = fields.iter().map(|field| { + let name = field.ident; + quote! { #name: self.#name.or(optb.#name) } + }); + + let try_from_fields = fields.iter().map(|field| { + let name = field.ident; + + match (field.is_nested, field.is_option) { + (false, false) => quote! { + #name: value.#name.ok_or(::optionalize::MissingRequiredField(stringify!(#name)))? + }, + (false, true) => quote! { + #name: value.#name + }, + (true, false) => quote! { + #name: value.#name.ok_or(::optionalize::MissingRequiredField(stringify!(#name)))?.try_into()? + }, + (true, true) => quote! { + #name: value.#name.try_into()? + }, + } + }); + + quote! { + #vis struct #opt { + #(#opt_fields),* + } + + impl ::optionalize::Optionalize for #opt { + type Base = #base; + + fn or(self, optb: Self) -> Self { + Self { + #(#or_fields),* + } + } + } + + impl ::std::convert::TryFrom<#opt> for #base { + type Error = ::optionalize::MissingRequiredField; + + fn try_from(value: #opt) -> ::std::result::Result { + Ok(Self { + #(#try_from_fields),* + }) + } + } + } +} diff --git a/optionalize-derive/src/util.rs b/optionalize-derive/src/util.rs new file mode 100644 index 0000000..c9c2d8b --- /dev/null +++ b/optionalize-derive/src/util.rs @@ -0,0 +1,100 @@ +use syn::{punctuated::Punctuated, spanned::Spanned}; + +pub(crate) fn struct_fields(struct_data: &syn::Data) -> &Punctuated { + match struct_data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(syn::FieldsNamed { named, .. }), + .. + }) => named, + _ => unimplemented!(), + } +} + +pub(crate) struct FieldInfo<'a> { + pub(crate) ident: Option<&'a syn::Ident>, + pub(crate) vis: &'a syn::Visibility, + pub(crate) is_nested: bool, + pub(crate) nested_opt_ty: Option, + pub(crate) is_option: bool, + pub(crate) underlying_ty: &'a syn::Type, +} + +pub(crate) fn struct_fields_info<'a>( + struct_data: &'a syn::Data, + helper: &'a str, +) -> Vec> { + struct_fields(struct_data) + .iter() + .map(|field| { + let inner_ty = get_generic_ty("Option", &field.ty); + let attr = helper_attr(&field.attrs, helper); + let ty = &field.ty; + + FieldInfo { + ident: field.ident.as_ref(), + vis: &field.vis, + is_nested: attr.is_some(), + nested_opt_ty: attr + .map(|attr| { + parse_name_path_attr(attr, helper).map(|(name, path)| { + if name != "name" { + panic!("usage of `opt` attribute is incorrect (unknown key is present)"); + } + path + }).unwrap_or(syn::Ident::new(&format!("{}Opt", quote::quote!(#ty)), field.span()).into()) + }), + is_option: inner_ty.is_some(), + underlying_ty: inner_ty.unwrap_or(&field.ty), + } + }) + .collect() +} + +pub(crate) fn get_generic_ty<'a>(wrapper: &str, ty: &'a syn::Type) -> Option<&'a syn::Type> { + if let syn::Type::Path(path) = ty { + let last_segment = path.path.segments.iter().last().unwrap(); + + if last_segment.ident != wrapper { + return None; + } + + let generic_arg = match &last_segment.arguments { + syn::PathArguments::AngleBracketed(inner) if inner.args.len() == 1 => &inner.args[0], + _ => return None, + }; + + match generic_arg { + syn::GenericArgument::Type(ty) => Some(ty), + _ => None, + } + } else { + None + } +} + +pub(crate) fn helper_attr<'a>( + attrs: &'a [syn::Attribute], + helper: &'a str, +) -> Option<&'a syn::Attribute> { + attrs.iter().find(|attr| attr.path().is_ident(helper)) +} + +pub(crate) fn parse_name_path_attr( + attr: &syn::Attribute, + helper: &str, +) -> Option<(String, syn::Path)> { + if attr.path().is_ident(helper) { + let name_value = match attr.parse_args::() { + Ok(syn::MetaNameValue { path, value, .. }) => (path.segments[0].ident.clone(), value), + _ => return None, + }; + let name = name_value.0.to_string(); + let path = match name_value.1 { + syn::Expr::Path(syn::ExprPath { path, .. }) => path, + _ => return None, + }; + Some((name, path)) + } else { + None + } +} diff --git a/optionalize/Cargo.toml b/optionalize/Cargo.toml new file mode 100644 index 0000000..14afd5c --- /dev/null +++ b/optionalize/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "optionalize" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib"] + +[dependencies] +optionalize-derive = { path = "../optionalize-derive" } diff --git a/optionalize/src/lib.rs b/optionalize/src/lib.rs new file mode 100644 index 0000000..deb3c99 --- /dev/null +++ b/optionalize/src/lib.rs @@ -0,0 +1,16 @@ +pub use optionalize_derive::Optionalize; + +pub trait Optionalize: Sized { + type Base; + fn or(self, optb: Self) -> Self; +} + +#[derive(Debug)] +pub struct MissingRequiredField(pub &'static str); + +impl std::fmt::Display for MissingRequiredField { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "required field `{}` is not set", self.0) + } +} +impl std::error::Error for MissingRequiredField {} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6ed0060 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,27 @@ +#![allow(dead_code)] + +use optionalize::Optionalize; + +#[derive(Optionalize)] +#[opt(name = FooFoo)] +struct Foo { + field_one: u16, + field_two: Option, + #[opt] + bar: Bar, +} + +#[derive(Optionalize)] +pub(crate) struct Bar { + pub field_three: Option, + #[opt(name = Zzzz)] + baz: Baz, +} + +#[derive(Optionalize)] +#[opt(name = Zzzz)] +pub struct Baz { + pub(crate) a: Box, +} + +fn main() {}