mirror of
https://github.com/kristoferssolo/filecaster.git
synced 2025-10-21 19:00:34 +00:00
feat: implement grammar
This commit is contained in:
parent
aaf5a081c1
commit
3d6d32af49
@ -1,4 +1,4 @@
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use unsynn::{Ident, TokenStream};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StructInfo {
|
||||
|
||||
@ -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<TokenStream, FromFileError> {
|
||||
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::Shadow>) -> Self {
|
||||
let file = file.unwrap_or_default();
|
||||
Self {
|
||||
#(#assignments),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #generics From<Option<#file_ident #generics>> for #name #generics {
|
||||
fn from(value: Option<#file_ident #generics>) -> Self {
|
||||
<Self as filecaster::FromFile>::from_file(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl #generics From<#file_ident #generics> for #name #generics {
|
||||
fn from(value: #file_ident #generics) -> Self {
|
||||
<Self as filecaster::FromFile>::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),* )] }
|
||||
}
|
||||
@ -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!()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<TokenStream>,
|
||||
pub name: Ident,
|
||||
pub colon: Colon,
|
||||
pub ty: TokenStream
|
||||
pub struct Attribute {
|
||||
pub pound: Pound, // #
|
||||
pub bracket_group: BracketGroupContaining<TokenStream> // [attr("value")]
|
||||
}
|
||||
|
||||
pub struct StructBody(pub CommaDelimitedVec<Field>);
|
||||
pub struct Field {
|
||||
pub attrs: Option<Vec<Attribute>>, // #[attr("value")]
|
||||
pub vis: Optional<KwPub>, // pub
|
||||
pub name: Ident, // bar
|
||||
pub colon: Colon, // :
|
||||
pub ty: Ident // String
|
||||
}
|
||||
|
||||
pub struct StructBody(pub CommaDelimitedVec<Field>); // all fields
|
||||
|
||||
pub struct StructDef {
|
||||
pub vis: TokenStream,
|
||||
pub kw_struct: Ident,
|
||||
pub name: Ident,
|
||||
pub generics: TokenStream,
|
||||
pub vis: Optional<KwPub>, // pub
|
||||
pub kw_struct: KwStruct, // "struct" keyword
|
||||
pub name: Ident, // Foo
|
||||
pub generics: Optional<BracketGroupContaining<TokenStream>>,
|
||||
pub body: BraceGroupContaining<StructBody>
|
||||
}
|
||||
}
|
||||
|
||||
#[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::<Attribute>()
|
||||
.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::<Field>().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::<StructDef>()
|
||||
.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::<StructDef>();
|
||||
|
||||
assert!(res.is_err(), "expected parse error for incomplete struct");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<TokenStream, FromFileError> {
|
||||
let info = parse_scruct_info(input)?;
|
||||
generate_impl(&info)
|
||||
todo!()
|
||||
}
|
||||
|
||||
@ -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<StructInfo, FromFileError> {
|
||||
let mut iter = input.to_token_iter();
|
||||
let def = iter
|
||||
.parse::<StructDef>()
|
||||
.map_err(|_| FromFileError::NotNamedStruct {
|
||||
span: Span::call_site(),
|
||||
})?;
|
||||
|
||||
Ok(def.into())
|
||||
}
|
||||
|
||||
pub fn parse_from_file_default_attr(
|
||||
attrs: &[AttributeInfo],
|
||||
) -> Result<Option<TokenStream>, 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<TokenStream> {
|
||||
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<I>(iter: &mut Peekable<I>) -> TokenStream
|
||||
where
|
||||
I: Iterator<Item = TokenTree>,
|
||||
{
|
||||
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<StructDef> 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<Field> 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()))
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user