nichlink/registry_core/syntax/
fields.rs1use std::collections::BTreeMap;
13
14use proc_macro2::{Delimiter, Span, TokenStream, TokenTree};
15
16use super::tokens::{literal_string, location, only_group, path_to_string, split_top_level};
17use super::{
18 FaceSyntax, FaceSyntaxError, FieldSyntax, ParentSyntax, SyntaxLocation, compact,
19 split_face_fields,
20};
21
22impl FaceSyntax {
23 pub fn field(&self, name: &str) -> Option<String> {
31 self.fields.get(name).map(|field| compact(&field.tokens))
32 }
33
34 pub fn cfg(&self) -> Option<&str> {
37 self.cfg.as_deref()
38 }
39
40 pub fn field_location(&self, name: &str) -> Option<&SyntaxLocation> {
43 self.fields.get(name).map(|field| &field.location)
44 }
45
46 pub fn path(&self, name: &str) -> Option<String> {
49 let field = self.fields.get(name)?;
50 syn::parse2::<syn::Path>(field.tokens.clone())
51 .ok()
52 .map(|path| path_to_string(&path))
53 }
54
55 pub fn string(&self, name: &str) -> Option<String> {
58 let field = self.fields.get(name)?;
59 syn::parse2::<syn::LitStr>(field.tokens.clone())
60 .ok()
61 .map(|literal| literal.value())
62 }
63
64 pub fn boolean(&self, name: &str) -> Option<bool> {
67 let field = self.fields.get(name)?;
68 syn::parse2::<syn::LitBool>(field.tokens.clone())
69 .ok()
70 .map(|literal| literal.value)
71 }
72
73 pub fn localized(&self, name: &str, language: &str) -> Option<String> {
77 let field = self.fields.get(name)?;
78 let group = only_group(&field.tokens, Delimiter::Brace)?;
79 parse_fields(group.stream(), group.span())
80 .ok()?
81 .get(language)
82 .and_then(|field| syn::parse2::<syn::LitStr>(field.tokens.clone()).ok())
83 .map(|literal| literal.value())
84 }
85
86 pub fn string_list(&self, name: &str) -> Option<Vec<String>> {
103 match name {
104 "handle_traits" => return self.trait_labels("handle_contracts", "handle_traits"),
105 "part_traits" => return self.trait_labels("part_contracts", "part_traits"),
106 _ => {}
107 }
108 self.written_string_list(name)
109 }
110
111 fn trait_labels(&self, path_field: &str, label_field: &str) -> Option<Vec<String>> {
115 let paths = self.path_list(path_field).unwrap_or_default();
116 if paths.is_empty() {
117 return self.written_string_list(label_field);
118 }
119 crate::authoring::parse::trait_names_from_paths(&paths.join(","))
120 .ok()
121 .map(|labels| {
122 labels
123 .split(',')
124 .map(str::trim)
125 .filter(|label| !label.is_empty())
126 .map(str::to_owned)
127 .collect()
128 })
129 }
130
131 fn written_string_list(&self, name: &str) -> Option<Vec<String>> {
134 let field = self.fields.get(name)?;
135 let group = only_group(&field.tokens, Delimiter::Bracket)?;
136 split_top_level(group.stream())
137 .into_iter()
138 .map(|tokens| syn::parse2::<syn::LitStr>(tokens).map(|literal| literal.value()))
139 .collect::<Result<Vec<_>, _>>()
140 .ok()
141 }
142
143 pub fn path_list(&self, name: &str) -> Option<Vec<String>> {
146 let field = self.fields.get(name)?;
147 let group = only_group(&field.tokens, Delimiter::Bracket)?;
148 split_top_level(group.stream())
149 .into_iter()
150 .map(|tokens| syn::parse2::<syn::Path>(tokens).map(|path| path_to_string(&path)))
151 .collect::<Result<Vec<_>, _>>()
152 .ok()
153 }
154
155 pub fn requirements(&self, name: &str) -> Option<Vec<(String, String)>> {
158 let field = self.fields.get(name)?;
159 let group = only_group(&field.tokens, Delimiter::Bracket)?;
160 split_top_level(group.stream())
161 .into_iter()
162 .map(parse_requirement)
163 .collect::<Option<Vec<_>>>()
164 }
165
166 pub fn option_string(&self, name: &str) -> Option<Option<String>> {
176 let field = self.fields.get(name)?;
177 let expression = syn::parse2::<syn::Expr>(field.tokens.clone()).ok()?;
178 match expression {
179 syn::Expr::Path(path) if path.path.is_ident("None") => Some(None),
180 syn::Expr::Call(call) => {
181 let syn::Expr::Path(function) = *call.func else {
182 return None;
183 };
184 if !function.path.is_ident("Some") || call.args.len() != 1 {
185 return None;
186 }
187 let syn::Expr::Lit(argument) = call.args.first()? else {
188 return None;
189 };
190 let syn::Lit::Str(value) = &argument.lit else {
191 return None;
192 };
193 Some(Some(value.value()))
194 }
195 _ => None,
196 }
197 }
198
199 pub fn parent(&self) -> Option<ParentSyntax> {
204 let field = self.fields.get("parent")?;
205 let expression = syn::parse2::<syn::Expr>(field.tokens.clone()).ok()?;
206 match expression {
207 syn::Expr::Path(path) => {
208 let path = path_to_string(&path.path);
209 if path.ends_with("ROOT_NODE_ID") {
210 Some(ParentSyntax::Root)
211 } else {
212 path.strip_suffix("::NODE_ID")
213 .map(|module| ParentSyntax::NodePath(module.to_owned()))
214 }
215 }
216 syn::Expr::Call(call) => parse_parent_call(call),
217 _ => None,
218 }
219 }
220}
221
222pub(super) fn parse_fields(
225 tokens: TokenStream,
226 fallback_span: Span,
227) -> Result<BTreeMap<String, FieldSyntax>, FaceSyntaxError> {
228 let mut fields = BTreeMap::new();
229 for field in split_face_fields(tokens) {
230 let mut tokens = field.into_iter();
231 let Some(TokenTree::Ident(name)) = tokens.next() else {
232 return Err(super::syntax_error(
233 fallback_span,
234 "expected a registration field name",
235 ));
236 };
237 let Some(TokenTree::Punct(colon)) = tokens.next() else {
238 return Err(super::syntax_error(
239 name.span(),
240 format!("expected `:` after `{name}`"),
241 ));
242 };
243 if colon.as_char() != ':' {
244 return Err(super::syntax_error(
245 colon.span(),
246 format!("expected `:` after `{name}`"),
247 ));
248 }
249 let value = tokens.collect::<TokenStream>();
250 if value.is_empty() {
251 return Err(super::syntax_error(
252 name.span(),
253 format!("field `{name}` has no value"),
254 ));
255 }
256 let field_name = name.to_string();
257 if fields
258 .insert(
259 field_name.clone(),
260 FieldSyntax {
261 tokens: value,
262 location: location(name.span()),
263 },
264 )
265 .is_some()
266 {
267 return Err(super::syntax_error(
268 name.span(),
269 format!("duplicate field `{field_name}`"),
270 ));
271 }
272 }
273 Ok(fields)
274}
275
276fn parse_requirement(tokens: TokenStream) -> Option<(String, String)> {
279 let tokens = tokens.into_iter().collect::<Vec<_>>();
280 let arrow = tokens.windows(2).position(|pair| {
281 matches!(&pair[0], TokenTree::Punct(punct) if punct.as_char() == '=')
282 && matches!(&pair[1], TokenTree::Punct(punct) if punct.as_char() == '>')
283 })?;
284 let left = tokens[..arrow].iter().cloned().collect::<TokenStream>();
285 let right = tokens[arrow + 2..].iter().cloned().collect::<TokenStream>();
286 let capability = syn::parse2::<syn::LitStr>(left).ok()?.value();
287 let provider = syn::parse2::<syn::LitStr>(right).ok()?.value();
288 Some((capability, provider))
289}
290
291fn parse_parent_call(call: syn::ExprCall) -> Option<ParentSyntax> {
294 let syn::Expr::Path(function) = *call.func else {
295 return None;
296 };
297 let function = path_to_string(&function.path);
298 if function.ends_with("root_node_id") && call.args.len() == 1 {
299 return Some(ParentSyntax::Root);
300 }
301 if !function.ends_with("NodeId::from_path") || call.args.len() != 2 {
302 return None;
303 }
304 let mut arguments = call.args.iter();
305 let source = literal_string(arguments.next()?)?;
306 let kind = literal_string(arguments.next()?)?;
307 Some(ParentSyntax::FromPath { source, kind })
308}