initial commit
This commit is contained in:
commit
76358fc961
9 changed files with 340 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
61
Cargo.lock
generated
Normal file
61
Cargo.lock
generated
Normal 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
7
Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
optionalize = { path = "optionalize" }
|
12
optionalize-derive/Cargo.toml
Normal file
12
optionalize-derive/Cargo.toml
Normal 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"
|
106
optionalize-derive/src/lib.rs
Normal file
106
optionalize-derive/src/lib.rs
Normal 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),*
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
optionalize-derive/src/util.rs
Normal file
100
optionalize-derive/src/util.rs
Normal 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
10
optionalize/Cargo.toml
Normal 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
16
optionalize/src/lib.rs
Normal 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
27
src/main.rs
Normal 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() {}
|
Loading…
Add table
Reference in a new issue