diff --git a/filecaster-derive/src/from_file/ast.rs b/filecaster-derive/src/from_file/ast.rs index 6fb31df..71d5a2b 100644 --- a/filecaster-derive/src/from_file/ast.rs +++ b/filecaster-derive/src/from_file/ast.rs @@ -1,4 +1,4 @@ -use proc_macro2::{Ident, TokenStream}; +use unsynn::{Ident, TokenStream}; #[derive(Debug)] pub struct StructInfo { diff --git a/filecaster-derive/src/from_file/codegen.rs b/filecaster-derive/src/from_file/codegen.rs deleted file mode 100644 index 28a45c4..0000000 --- a/filecaster-derive/src/from_file/codegen.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::from_file::{ - ast::StructInfo, error::FromFileError, parser::parse_from_file_default_attr, -}; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; - -pub fn generate_impl(info: &StructInfo) -> Result { - let name = &info.ident; - let vis = &info.vis; - let generics = &info.generics; - let file_ident = format_ident!("{name}File"); - - let mut file_fields = Vec::new(); - let mut assignments = Vec::new(); - - for field in &info.fields { - let ident = &field.ident; - let ty = &field.ty; - let default_override = parse_from_file_default_attr(&field.attrs)?; - - let shadow_ty = quote! { <#ty as fielcaster::FromFile>::Shadow }; - file_fields.push(quote! { pub #ident: Option<#shadow_ty> }); - - if let Some(expr) = default_override { - assignments.push(quote! { - #ident: fide.#ident - .map(|inner| <#ty as filecaster::FromFile>::from_file(Some(inner))) - .unwrap_or(#expr) - }); - } else { - assignments.push(quote! { - #ident: <#ty as filecaster::FromFile>::from_file(file.#ident) - }); - } - } - - let derive_clause = build_derive_clause(); - - Ok(quote! { - #derive_clause - #vis struct #file_ident #generics { - #(#file_fields),* - } - - impl #generics filecaster::FromFile for #name #generics { - type Shadow = #file_ident #generics; - - fn from_file(file: Option) -> Self { - let file = file.unwrap_or_default(); - Self { - #(#assignments),* - } - } - } - - impl #generics From> for #name #generics { - fn from(value: Option<#file_ident #generics>) -> Self { - ::from_file(value) - } - } - - impl #generics From<#file_ident #generics> for #name #generics { - fn from(value: #file_ident #generics) -> Self { - ::from_file(Some(value)) - } - } - }) -} - -fn build_derive_clause() -> TokenStream { - let mut traits = vec![quote! {Debug}, quote! {Clone}, quote! {Default}]; - #[cfg(feature = "serde")] - { - traits.push(quote! { serde::Deserialize }); - traits.push(quote! { serde::Serialize }); - } - - #[cfg(feature = "merge")] - { - traits.push(quote! { merge::Merge }); - } - - quote! { #[derive( #(#traits),* )] } -} diff --git a/filecaster-derive/src/from_file/error.rs b/filecaster-derive/src/from_file/error.rs index 0f2278a..1d3fc00 100644 --- a/filecaster-derive/src/from_file/error.rs +++ b/filecaster-derive/src/from_file/error.rs @@ -1,22 +1,11 @@ -use proc_macro2::{Span, TokenStream}; -use quote::quote_spanned; use thiserror::Error; +use unsynn::TokenStream; #[derive(Debug, Error)] -pub enum FromFileError { - #[error("FromFile only works on structs with named fields")] - NotNamedStruct { span: Span }, - #[error("Invalid #[from_file] attribute format")] - InvalidAttribute { span: Span }, -} +pub enum FromFileError {} impl FromFileError { pub fn to_compile_error(&self) -> TokenStream { - let msg = self.to_string(); - match self { - FromFileError::NotNamedStruct { span } | FromFileError::InvalidAttribute { span } => { - quote_spanned!(*span => compile_error!(#msg)) - } - } + todo!() } } diff --git a/filecaster-derive/src/from_file/grammar.rs b/filecaster-derive/src/from_file/grammar.rs index 5b0a917..1b356f5 100644 --- a/filecaster-derive/src/from_file/grammar.rs +++ b/filecaster-derive/src/from_file/grammar.rs @@ -1,20 +1,127 @@ use unsynn::*; +keyword! { + KwStruct = "struct"; + KwPub = "pub"; +} + +/* +pub struct Foo { + #[attr("value")] + pub bar: String, +} +*/ unsynn! { - pub struct Field { - pub attrs: Vec, - pub name: Ident, - pub colon: Colon, - pub ty: TokenStream + pub struct Attribute { + pub pound: Pound, // # + pub bracket_group: BracketGroupContaining // [attr("value")] } - pub struct StructBody(pub CommaDelimitedVec); + pub struct Field { + pub attrs: Option>, // #[attr("value")] + pub vis: Optional, // pub + pub name: Ident, // bar + pub colon: Colon, // : + pub ty: Ident // String + } + + pub struct StructBody(pub CommaDelimitedVec); // all fields pub struct StructDef { - pub vis: TokenStream, - pub kw_struct: Ident, - pub name: Ident, - pub generics: TokenStream, + pub vis: Optional, // pub + pub kw_struct: KwStruct, // "struct" keyword + pub name: Ident, // Foo + pub generics: Optional>, pub body: BraceGroupContaining } } + +#[cfg(test)] +mod tests { + use super::*; + use claims::assert_some; + + const SAMPLE: &str = r#" + pub struct Foo { + #[attr("value")] + pub bar: String, + #[attr("number")] + pub baz: i32 + } +"#; + + #[test] + fn parse_attribute_roundup() { + let mut iter = r#"#[attr("value")]"#.to_token_iter(); + let attr = iter + .parse::() + .expect("failed to parse Attribute"); + + assert_eq!(attr.pound.tokens_to_string(), "#".tokens_to_string()); + + let s = attr.bracket_group.tokens_to_string(); + assert!(s.contains("attr")); + assert!(s.contains("\"value\"")); + + let rt = attr.tokens_to_string(); + assert!(rt.contains("attr")); + assert!(rt.contains("\"value\"")); + } + + #[test] + fn parse_field_with_attr_and_vid_and_roundtrip() { + let mut iter = r#"#[attr("value")] pub bar: String"#.to_token_iter(); + let field = iter.parse::().expect("failed to parse Field"); + + assert_some!(&field.attrs); + let attrs = field.attrs.as_ref().unwrap(); + assert_eq!(attrs.len(), 1); + assert_eq!(field.name.tokens_to_string(), "bar".tokens_to_string()); + assert_eq!(field.ty.tokens_to_string(), "String".tokens_to_string()); + + let tokens = field.tokens_to_string(); + assert!(tokens.contains("attr")); + assert!(tokens.contains("bar")); + assert!(tokens.contains("String")); + } + + #[test] + fn parse_struct_def_and_inspect_body() { + let mut iter = SAMPLE.to_token_iter(); + dbg!(&iter); + + let sdef = iter + .parse::() + .expect("faield to parse StructDef"); + + assert_eq!( + sdef.kw_struct.tokens_to_string(), + "struct".tokens_to_string() + ); + assert_eq!(sdef.name.tokens_to_string(), "Foo".tokens_to_string()); + + let body = &sdef.body.content.0.0; + assert_eq!(body.len(), 2); + + let field = &body[0].value; + assert_some!(field.attrs.as_ref()); + assert_eq!(field.name.tokens_to_string(), "bar".tokens_to_string()); + assert_eq!(field.ty.tokens_to_string(), "String".tokens_to_string()); + + let out = sdef.tokens_to_string(); + assert!(out.contains("pub")); + assert!(out.contains("struct")); + assert!(out.contains("Foo")); + assert!(out.contains("attr")); + assert!(out.contains("bar")); + assert!(out.contains("String")); + } + + #[test] + fn parse_failure_for_incomplete_struct() { + let mut iter = "pub struct Foo".to_token_iter(); + let res = iter.parse::(); + + assert!(res.is_err(), "expected parse error for incomplete struct"); + } +} diff --git a/filecaster-derive/src/from_file/mod.rs b/filecaster-derive/src/from_file/mod.rs index d33ba8a..78e4333 100644 --- a/filecaster-derive/src/from_file/mod.rs +++ b/filecaster-derive/src/from_file/mod.rs @@ -1,13 +1,11 @@ mod ast; -mod codegen; mod error; mod grammar; mod parser; -use crate::from_file::{codegen::generate_impl, error::FromFileError, parser::parse_scruct_info}; -use proc_macro2::TokenStream; +use crate::from_file::error::FromFileError; +use unsynn::TokenStream; pub fn impl_from_file(input: TokenStream) -> Result { - let info = parse_scruct_info(input)?; - generate_impl(&info) + todo!() } diff --git a/filecaster-derive/src/from_file/parser.rs b/filecaster-derive/src/from_file/parser.rs index f07d478..e69de29 100644 --- a/filecaster-derive/src/from_file/parser.rs +++ b/filecaster-derive/src/from_file/parser.rs @@ -1,118 +0,0 @@ -use std::iter::{Peekable, once}; - -use crate::from_file::ast::AttributeInfo; -use crate::from_file::grammar::{Field, StructDef}; -use crate::from_file::{ - ast::{FieldInfo, StructInfo}, - error::FromFileError, -}; -use proc_macro2::{Ident, Span, TokenStream}; -use unsynn::TokenTree; -use unsynn::{IParse, ToTokens}; - -pub fn parse_scruct_info(input: TokenStream) -> Result { - let mut iter = input.to_token_iter(); - let def = iter - .parse::() - .map_err(|_| FromFileError::NotNamedStruct { - span: Span::call_site(), - })?; - - Ok(def.into()) -} - -pub fn parse_from_file_default_attr( - attrs: &[AttributeInfo], -) -> Result, FromFileError> { - for attr in attrs { - if attr.path == "from_file" { - return extract_default_token(attr.tokens.clone()) - .map(Some) - .ok_or_else(|| FromFileError::InvalidAttribute { - span: attr.path.span(), - }); - } - } - Ok(None) -} - -fn extract_default_token(tokens: TokenStream) -> Option { - let mut iter = tokens.into_iter().peekable(); - - while let Some(TokenTree::Ident(id)) = iter.next() { - if id != "default" { - continue; - } - - match iter.next() { - Some(TokenTree::Punct(eq)) if eq.as_char() == '=' => { - return Some(collect_until_commas(&mut iter)); - } - _ => return None, - } - } - None -} - -fn collect_until_commas(iter: &mut Peekable) -> TokenStream -where - I: Iterator, -{ - let mut expr = TokenStream::new(); - while let Some(tt) = iter.peek() { - let is_comma = matches!(tt, TokenTree::Punct(p) if p.as_char() ==','); - if is_comma { - iter.next(); - break; - } - expr.extend(once(iter.next().unwrap())); - } - expr -} - -impl From for StructInfo { - fn from(value: StructDef) -> Self { - Self { - ident: value.name, - vis: value.vis, - generics: value.generics, - fields: value - .body - .content - .0 - .into_iter() - .map(|d| d.value.into()) - .collect(), - } - } -} - -impl From for FieldInfo { - fn from(value: Field) -> Self { - Self { - ident: value.name, - ty: value.ty, - attrs: value - .attrs - .into_iter() - .map(|ts| { - let path = extract_attr_path(ts.clone()); - AttributeInfo { path, tokens: ts } - }) - .collect(), - } - } -} - -fn extract_attr_path(attr_tokens: TokenStream) -> Ident { - attr_tokens - .into_iter() - .find_map(|tt| { - if let TokenTree::Ident(id) = tt { - Some(id) - } else { - None - } - }) - .unwrap_or_else(|| Ident::new("unknown", Span::call_site())) -} diff --git a/filecaster-derive/src/lib.rs b/filecaster-derive/src/lib.rs index 0a7827b..df2602c 100644 --- a/filecaster-derive/src/lib.rs +++ b/filecaster-derive/src/lib.rs @@ -96,11 +96,10 @@ mod from_file; +use crate::from_file::impl_from_file; use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; -use crate::from_file::impl_from_file; - /// Implements the [`FromFile`] trait. /// /// This macro processes the `#[from_file]` attribute on structs to generate