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)]
|
#[derive(Debug)]
|
||||||
pub struct StructInfo {
|
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 thiserror::Error;
|
||||||
|
use unsynn::TokenStream;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FromFileError {
|
pub enum FromFileError {}
|
||||||
#[error("FromFile only works on structs with named fields")]
|
|
||||||
NotNamedStruct { span: Span },
|
|
||||||
#[error("Invalid #[from_file] attribute format")]
|
|
||||||
InvalidAttribute { span: Span },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromFileError {
|
impl FromFileError {
|
||||||
pub fn to_compile_error(&self) -> TokenStream {
|
pub fn to_compile_error(&self) -> TokenStream {
|
||||||
let msg = self.to_string();
|
todo!()
|
||||||
match self {
|
|
||||||
FromFileError::NotNamedStruct { span } | FromFileError::InvalidAttribute { span } => {
|
|
||||||
quote_spanned!(*span => compile_error!(#msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,127 @@
|
|||||||
use unsynn::*;
|
use unsynn::*;
|
||||||
|
|
||||||
|
keyword! {
|
||||||
|
KwStruct = "struct";
|
||||||
|
KwPub = "pub";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub struct Foo {
|
||||||
|
#[attr("value")]
|
||||||
|
pub bar: String,
|
||||||
|
}
|
||||||
|
*/
|
||||||
unsynn! {
|
unsynn! {
|
||||||
pub struct Field {
|
pub struct Attribute {
|
||||||
pub attrs: Vec<TokenStream>,
|
pub pound: Pound, // #
|
||||||
pub name: Ident,
|
pub bracket_group: BracketGroupContaining<TokenStream> // [attr("value")]
|
||||||
pub colon: Colon,
|
|
||||||
pub ty: TokenStream
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 struct StructDef {
|
||||||
pub vis: TokenStream,
|
pub vis: Optional<KwPub>, // pub
|
||||||
pub kw_struct: Ident,
|
pub kw_struct: KwStruct, // "struct" keyword
|
||||||
pub name: Ident,
|
pub name: Ident, // Foo
|
||||||
pub generics: TokenStream,
|
pub generics: Optional<BracketGroupContaining<TokenStream>>,
|
||||||
pub body: BraceGroupContaining<StructBody>
|
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 ast;
|
||||||
mod codegen;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod grammar;
|
mod grammar;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
use crate::from_file::{codegen::generate_impl, error::FromFileError, parser::parse_scruct_info};
|
use crate::from_file::error::FromFileError;
|
||||||
use proc_macro2::TokenStream;
|
use unsynn::TokenStream;
|
||||||
|
|
||||||
pub fn impl_from_file(input: TokenStream) -> Result<TokenStream, FromFileError> {
|
pub fn impl_from_file(input: TokenStream) -> Result<TokenStream, FromFileError> {
|
||||||
let info = parse_scruct_info(input)?;
|
todo!()
|
||||||
generate_impl(&info)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
mod from_file;
|
||||||
|
|
||||||
|
use crate::from_file::impl_from_file;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro_error2::proc_macro_error;
|
use proc_macro_error2::proc_macro_error;
|
||||||
|
|
||||||
use crate::from_file::impl_from_file;
|
|
||||||
|
|
||||||
/// Implements the [`FromFile`] trait.
|
/// Implements the [`FromFile`] trait.
|
||||||
///
|
///
|
||||||
/// This macro processes the `#[from_file]` attribute on structs to generate
|
/// This macro processes the `#[from_file]` attribute on structs to generate
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user