1#![doc = include_str!("../README.md")]
2
3extern crate proc_macro;
4
5use std::path::PathBuf;
6
7use proc_macro::TokenStream;
8use rhai::{Engine, EvalAltResult, ImmutableString, Position, Scope};
9use syn::{
10 parse::{Parse, ParseStream},
11 parse_macro_input, LitStr,
12};
13
14struct RunScriptInput {
15 script_source: LitStr,
16}
17
18impl Parse for RunScriptInput {
19 fn parse(input: ParseStream) -> syn::Result<Self> {
20 Ok(Self {
21 script_source: input.parse()?,
22 })
23 }
24}
25
26fn get_source_context(source_code: &str, padding: usize, pos: Position) -> String {
27 let mut source_snippet = String::new();
28
29 if let Some(lineno) = pos.line() {
30 let lines: Vec<_> = source_code.split('\n').collect();
31 for (i, line) in lines
32 [(lineno - padding).clamp(0, lines.len())..(lineno + padding).clamp(0, lines.len())]
33 .iter()
34 .enumerate()
35 {
36 if i == padding - 1 {
37 source_snippet.push_str("--> ");
38 } else {
39 source_snippet.push_str(" ");
40 }
41
42 source_snippet.push_str(line);
43 source_snippet.push('\n');
44 }
45 }
46
47 source_snippet
48}
49
50fn handle_runtime_error(source_code: &str, e: Box<EvalAltResult>) {
51 let pos = {
52 let mut inner_error = &e;
53
54 while let EvalAltResult::ErrorInModule(_, err, ..)
55 | EvalAltResult::ErrorInFunctionCall(_, _, err, ..) = &**inner_error
56 {
57 inner_error = err;
58 }
59
60 inner_error.position()
61 };
62
63 panic!("{}\n\n{}", e, get_source_context(source_code, 3, pos));
64}
65
66#[proc_macro]
67pub fn run_script(params: TokenStream) -> TokenStream {
68 let args = parse_macro_input!(params as RunScriptInput);
69
70 let engine = get_default_engine();
71 let output: String = engine
72 .eval(&args.script_source.value())
73 .map_err(|e| handle_runtime_error(&args.script_source.value(), e))
74 .unwrap();
75
76 output.parse().expect("invalid token stream")
77}
78
79#[proc_macro_attribute]
80pub fn run_script_on(params: TokenStream, item: TokenStream) -> TokenStream {
81 let args = parse_macro_input!(params as RunScriptInput);
82 let engine = get_default_engine();
83
84 let mut scope = Scope::new();
85 scope.push("item", item.to_string());
86 let output: String = engine
87 .eval_with_scope(&mut scope, &args.script_source.value())
88 .map_err(|e| handle_runtime_error(&args.script_source.value(), e))
89 .unwrap();
90
91 output.parse().expect("invalid token stream")
92}
93
94fn get_default_engine() -> Engine {
95 let mut engine = Engine::new();
96
97 engine.set_max_expr_depths(100, 100);
98
99 #[cfg(feature = "parse-yaml")]
100 engine.register_fn("parse_yaml", helper_parse_yaml);
101 #[cfg(feature = "parse-json")]
102 engine.register_fn("parse_json", helper_parse_json);
103 #[cfg(feature = "parse-yaml")]
104 engine.register_fn("stringify_yaml", helper_stringify_yaml);
105 #[cfg(feature = "parse-json")]
106 engine.register_fn("stringify_json", helper_stringify_json);
107 engine.register_fn("slugify_ident", helper_slugify_ident);
108 #[cfg(feature = "glob")]
109 engine.register_fn("glob", helper_glob);
110 engine.register_fn("basename", helper_basename);
111
112 #[cfg(feature = "filesystem")]
113 {
114 use rhai::packages::Package;
115 use rhai_fs::FilesystemPackage;
116 let package = FilesystemPackage::new();
117 package.register_into_engine(&mut engine);
118 }
119
120 engine
121}
122
123#[cfg(any(
124 feature = "parse-yaml",
125 feature = "parse-json",
126 feature = "filesystem",
127 feature = "glob",
128))]
129fn coerce_err(x: impl std::fmt::Debug) -> Box<EvalAltResult> {
130 format!("{x:?}").into()
131}
132
133#[cfg(feature = "parse-yaml")]
134fn helper_parse_yaml(input: ImmutableString) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
135 serde_yaml::from_str(input.as_str()).map_err(coerce_err)
136}
137
138#[cfg(feature = "parse-yaml")]
139fn helper_stringify_yaml(input: rhai::Dynamic) -> Result<ImmutableString, Box<EvalAltResult>> {
140 serde_yaml::to_string(&input)
141 .map(From::from)
142 .map_err(coerce_err)
143}
144
145#[cfg(feature = "parse-json")]
146fn helper_parse_json(input: ImmutableString) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
147 serde_json::from_str(input.as_str()).map_err(coerce_err)
148}
149
150#[cfg(feature = "parse-json")]
151fn helper_stringify_json(input: rhai::Dynamic) -> Result<ImmutableString, Box<EvalAltResult>> {
152 serde_json::to_string(&input)
153 .map(From::from)
154 .map_err(coerce_err)
155}
156
157fn helper_slugify_ident(input: ImmutableString) -> ImmutableString {
158 let mut is_first_char = true;
159 input
160 .as_str()
161 .replace(
162 |x: char| {
163 if is_first_char && x.is_ascii_digit() {
164 return true;
165 }
166 is_first_char = false;
167
168 !matches!(x, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_')
169 },
170 "_",
171 )
172 .into()
173}
174
175#[cfg(feature = "glob")]
176fn helper_glob(pattern: ImmutableString) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
177 let mut result = Vec::new();
178
179 for entry in glob::glob(pattern.as_str()).map_err(coerce_err)? {
180 let entry = entry.map_err(coerce_err)?;
181
182 result.push(entry);
183 }
184
185 Ok(result.into())
186}
187
188fn helper_basename(input: PathBuf) -> Result<ImmutableString, Box<EvalAltResult>> {
189 Ok(input
190 .file_name()
191 .unwrap_or(input.as_os_str())
192 .to_str()
193 .ok_or("basename is not valid unicode")?
194 .to_owned()
195 .into())
196}