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