initial commit

This commit is contained in:
naskya 2024-07-12 01:13:18 +09:00
commit 76358fc961
Signed by: naskya
GPG key ID: 712D413B3A9FED5C
9 changed files with 340 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

61
Cargo.lock generated Normal file
View file

@ -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"

7
Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "test"
version = "0.1.0"
edition = "2021"
[dependencies]
optionalize = { path = "optionalize" }

View file

@ -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"

View file

@ -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::<Vec<_>>();
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<Self, Self::Error> {
Ok(Self {
#(#try_from_fields),*
})
}
}
}
}

View file

@ -0,0 +1,100 @@
use syn::{punctuated::Punctuated, spanned::Spanned};
pub(crate) fn struct_fields(struct_data: &syn::Data) -> &Punctuated<syn::Field, syn::token::Comma> {
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<syn::Path>,
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<FieldInfo<'a>> {
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::<syn::MetaNameValue>() {
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
}
}

10
optionalize/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "optionalize"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["lib"]
[dependencies]
optionalize-derive = { path = "../optionalize-derive" }

16
optionalize/src/lib.rs Normal file
View file

@ -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 {}

27
src/main.rs Normal file
View file

@ -0,0 +1,27 @@
#![allow(dead_code)]
use optionalize::Optionalize;
#[derive(Optionalize)]
#[opt(name = FooFoo)]
struct Foo {
field_one: u16,
field_two: Option<String>,
#[opt]
bar: Bar,
}
#[derive(Optionalize)]
pub(crate) struct Bar {
pub field_three: Option<char>,
#[opt(name = Zzzz)]
baz: Baz,
}
#[derive(Optionalize)]
#[opt(name = Zzzz)]
pub struct Baz {
pub(crate) a: Box<Baz>,
}
fn main() {}