reached usable state with procedural macro

This commit is contained in:
2025-01-12 23:41:34 +01:00
parent d20fd0d023
commit f5197c6362
11 changed files with 229 additions and 179 deletions
@@ -1,51 +1,39 @@
#![allow(dead_code)]
use proc_macro2::TokenStream;
use quote::quote;
pub fn parse_config_file(input: &String) -> std::io::Result<std::collections::HashMap<String, String>> {
let mut config = std::collections::HashMap::<String, String>::new();
let lines = input.lines();
use syn::{Attribute, Error, Field, Meta, Path, punctuated::Punctuated, token::Comma};
for line in lines {
let line = line.trim();
// ignore empty lines
if line.is_empty() { continue; }
pub fn gen_config_load_function(fields: &Punctuated<Field, Comma>, config_map_name: &syn::Ident) -> Result<TokenStream, Error> {
let mut assignments : TokenStream = TokenStream::new();
'field_loop: for field in fields.iter() {
let attr = &field.attrs;
let name = match &field.ident {
Some(value) => value,
// skip anonymous fields
None => continue 'field_loop,
};
let name_string: String = name.to_string();
let ty = &field.ty;
for attribute in attr {
if let Attribute{ meta: Meta::Path( Path{segments: attr_name, ..} ), .. } = attribute {
match attr_name.first() {
Some(value) => if value.ident == "nested_config" {
assignments.extend(quote! {
self.#name.parse_from_map(&mut #config_map_name);
});
continue 'field_loop;
} else if value.ident == "no_config" {
continue 'field_loop;
},
None => (),
}
if line.starts_with("[") {
// check for table
if !line.ends_with("]") {
// TODO: implement error handling
} else if line.contains(' ') {
// TODO: implement error handling
}
//let tokens = line.split('.');
// TODO: implement hirarchical map
} else {
// check for config
let mut tokens: Vec<&str> = line.split('=').map(|entry| entry.trim()).collect();
tokens.retain(|entry| !entry.is_empty());
// check for valid input
if tokens.len() != 2 {
// println!("error in line'", line);
continue;
}
//// clean up value: remove quotes.. TODO:
//let mut options: Vec<&str> = tokens[1].split(['\'', '\"']).collect::<Vec<&str>>().join(',');
config.insert(tokens[0].to_string(), tokens[1].to_string());
}
assignments.extend(quote! {
self.#name = match #config_map_name.get(#name_string) {
Some(value) => match value.parse::<#ty>() {
Ok(parsed) => {
_ = #config_map_name.remove(#name_string);
parsed
},
Err(_) => {
self.#name.clone()
},
},
None => self.#name.clone(),
};
});
}
Ok(assignments.into())
Ok(config)
}
@@ -189,7 +189,7 @@ pub fn make_padding_string(len: usize) -> String {
/// input format is a quoted string (either double or single)
/// the style can be a combination of **one** color and
/// one or more style options (bold, italic, underlined, strikethrough)
pub fn parse_style(arg: String) -> Result<String> {
pub fn parse_style(arg: &String) -> Result<String> {
let mut colors: Vec<String> = Vec::<String>::new();
let mut styles: Vec<String> = Vec::<String>::new();
@@ -1,3 +1,5 @@
pub mod common;
pub mod format;
pub use common::parse_config_file;
@@ -0,0 +1,128 @@
use proc_macro2::{Ident, TokenStream};
use syn::{Attribute, Field, Meta, Path, punctuated::Punctuated, token::Comma};
use quote::quote;
pub fn gen_parse_from_string(config_name: &Ident, assignments: &TokenStream) -> TokenStream {
quote! {
/// tries to parse config from a string
/// if **convert_styles** == true, the settings marked with
/// `style_config` are converted to ansi escape sequences to
/// style terminal ouput
pub fn parse_from_string(&mut self, input: &String) -> std::io::Result<()> {
let mut #config_name : std::collections::HashMap<String, String> = parse_config_file(input)?;
#assignments
if !#config_name.is_empty() {
let leftovers = #config_name.keys().cloned().collect::<Vec<String>>();
return Err(std::io::Error::other(format!("the following settings were not recognised: {:#?}", leftovers)));
}
Ok(())
}
}
}
pub fn gen_parse_from_map(config_name: &Ident, assignments: &TokenStream) -> TokenStream {
quote! {
/// **do not call**
/// this function needs to be public for nested configs but is not intended
/// to be called by the user
pub fn parse_from_map(&mut self, input: &mut std::collections::HashMap<String, String>) -> std::io::Result<()> {
let mut #config_name = input;
#assignments
Ok(())
}
}
}
pub fn gen_to_ansi_sequences(fields: &Punctuated<Field, Comma>) -> TokenStream {
let mut conversions: TokenStream = TokenStream::new();
'fields: for field in fields.iter() {
let attr = &field.attrs;
let name = match &field.ident {
Some(value) => value,
// skip anonymous fields
None => continue 'fields,
};
for attribute in attr {
if let Attribute{ meta: Meta::Path( Path{segments: attr_name, ..} ), .. } = attribute {
match attr_name.first() {
Some(value) => if value.ident == "style_config" {
conversions.extend(quote! {
self.#name = match config_parser::parse_style(&self.#name) {
Ok(value) => value,
Err(_) => return Err(std::io::Error::other(format!("failed to convert '{}' to ansi escape sequence", self.#name))),
};
});
} else if value.ident == "nested_config" {
conversions.extend(quote! {
self.#name.to_ansi_sequences()?;
});
},
None => (),
}
}
}
};
quote!{
fn to_ansi_sequences(&mut self) -> std::io::Result<()> {
#conversions
Ok(())
}
}
}
pub fn gen_to_string(name: &Ident, fields: &Punctuated<Field, Comma>) -> TokenStream {
quote! {
/// prints configuration to `String`
//pub fn to_string() -> String {
// let mut default = "# default configuration file for `navigate`\n".to_string();
//}
}
}
pub fn gen_config_assignments(fields: &Punctuated<Field, Comma>, config_map_name: &syn::Ident) -> TokenStream {
let mut assignments : TokenStream = TokenStream::new();
'fields: for field in fields.iter() {
let attr = &field.attrs;
let name = match &field.ident {
Some(value) => value,
// skip anonymous fields
None => continue 'fields,
};
let name_string: String = name.to_string();
let ty = &field.ty;
for attribute in attr {
if let Attribute{ meta: Meta::Path( Path{segments: attr_name, ..} ), .. } = attribute {
match attr_name.first() {
Some(value) => if value.ident == "nested_config" {
assignments.extend(quote! {
self.#name.parse_from_map(&mut #config_map_name);
});
continue 'fields;
} else if value.ident == "no_config" {
continue 'fields;
},
None => (),
}
}
}
assignments.extend(quote! {
self.#name = match #config_map_name.get(#name_string) {
Some(value) => match value.parse::<#ty>() {
Ok(parsed) => {
parsed
},
Err(_) => {
self.#name.clone()
},
},
None => self.#name.clone(),
};
});
}
assignments
}
+16 -75
View File
@@ -1,14 +1,17 @@
use config_parser_common::common::gen_config_load_function;
mod generator_functions;
use proc_macro2::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use quote::quote;
use generator_functions::*;
/// **for structs only**
/// - implements `parse_config(&mut self, input: &String) -> Result<()>`
/// which parses a string and fills the fills recognised values into the struct
/// - implements `write_default_config() -> Result<String>`
/// which write a default configuration, in case the documentation is lacking
#[proc_macro_derive(ConfigParser, attributes(color_config, nested_config, no_config))]
#[proc_macro_derive(ConfigParser, attributes(nested_config, no_config, style_config))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
@@ -18,83 +21,21 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
} else {
panic!("the macro `ConfigParser` applies only to structs!");
};
let assignments = match gen_config_load_function(fields, &config_name) {
Ok(value) => value,
Err(_) => panic!("loading config failed"),
};
let assignments: TokenStream = gen_config_assignments(fields, &config_name);
let func_parse_string: TokenStream = gen_parse_from_string(&config_name, &assignments);
let func_parse_map: TokenStream = gen_parse_from_map(&config_name, &assignments);
let func_to_ansi_sequences: TokenStream = gen_to_ansi_sequences(fields);
//let func_to_string = gen_to_string(&name, &fields);
if name.to_string() == "Settings" {
let string = format!("{:#?}", ast);
_ = std::fs::write("test.txt", string);
}
let expanded_stream: TokenStream = quote! {
quote! {
impl #name {
/// tries to parse config from a string
pub fn parse_from_string(&mut self, input: &String) -> std::io::Result<()> {
let mut #config_name : std::collections::HashMap<String, String> = Self::parse_config_file(input)?;
#assignments
if !#config_name.is_empty() {
let leftovers = #config_name.keys().cloned().collect::<Vec<String>>();
return Err(std::io::Error::other(format!("the following settings were not recognised: {:#?}", leftovers)));
}
Ok(())
}
/// **do not call**
/// this function needs to be public for nested configs but is not intended
/// to be called by the user
pub fn parse_from_map(&mut self, input: &mut std::collections::HashMap<String, String>) -> std::io::Result<()> {
let mut #config_name = input;
#assignments
Ok(())
}
fn parse_config_file(input: &String) -> std::io::Result<std::collections::HashMap<String, String>> {
let mut config = std::collections::HashMap::<String, String>::new();
let lines = input.lines();
for line in lines {
let mut line = line.trim();
// ignore empty lines
if line.is_empty() { continue; }
if line.starts_with("[") {
// check for table
if !line.ends_with("]") {
// TODO: implement error handling
} else if line.contains(' ') {
// TODO: implement error handling
}
//let tokens = line.split('.');
// TODO: implement hirarchical map
} else {
// check for config
let mut tokens: Vec<&str> = line.split('=').map(|entry| entry.trim()).collect();
tokens.retain(|entry| !entry.is_empty());
if tokens.len() != 2 {
// println!("error in line'", line);
continue;
}
config.insert(tokens[0].to_string(), tokens[1].to_string());
}
}
Ok(config)
}
// TODO: implement
pub fn write_default_config(&self) -> Result<()> {
Ok(())
}
#func_parse_string
#func_parse_map
#func_to_ansi_sequences
//#func_to_string
// TODO: implement function to parse style settings
}
}.into();
expanded_stream.into()
}.into()
}
+1 -1
View File
@@ -1,3 +1,3 @@
pub use config_parser_common::format;
pub use config_parser_common::{common::*, format::*};
pub use config_parser_macro::ConfigParser;