1#![doc(
2 html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
3 html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
4)]
5
6use std::collections::HashMap;
7use std::fs::{self, File};
8use std::io::{self, Write};
9use std::path::PathBuf;
10
11use anyhow::{Context, Result};
12use clap::{Parser, ValueEnum};
13use serde::Serialize;
14use syn::{Expr, Lit, LitStr};
15
16#[derive(Clone, Copy, Debug, ValueEnum)]
17enum SchemaFormat {
18 Json,
19 Yaml,
20}
21
22#[derive(Clone, Copy, Debug, Serialize)]
23enum MetricType {
24 Timer,
25 Counter,
26 Histogram,
27 Set,
28 Gauge,
29}
30
31#[derive(Debug, PartialEq, Eq, Hash)]
32struct MetricPath(syn::Ident, syn::Ident);
33
34#[derive(Debug, Serialize)]
35struct Metric {
36 #[serde(rename = "type")]
37 ty: MetricType,
38 name: String,
39 description: String,
40 features: Vec<String>,
41}
42
43fn get_attr(name: &str, nv: &syn::MetaNameValue) -> Option<String> {
45 let Expr::Lit(lit) = &nv.value else {
46 return None;
47 };
48 match &lit.lit {
49 syn::Lit::Str(s) if nv.path.is_ident(name) => Some(s.value()),
50 _ => None,
51 }
52}
53
54fn add_doc_line(docs: &mut String, nv: &syn::MetaNameValue) {
56 if let Some(line) = get_attr("doc", nv) {
57 if !docs.is_empty() {
58 docs.push('\n');
59 }
60 docs.push_str(line.trim());
61 }
62}
63
64fn add_feature(features: &mut Vec<String>, l: &syn::MetaList) -> Result<()> {
66 if l.path.is_ident("cfg") {
67 l.parse_nested_meta(|meta| process_meta_item(features, &meta))?;
68 }
69 Ok(())
70}
71
72fn process_meta_item(
74 features: &mut Vec<String>,
75 meta: &syn::meta::ParseNestedMeta,
76) -> syn::Result<()> {
77 if meta.path.is_ident("feature") {
78 let s = meta.value()?.parse::<LitStr>()?;
79 features.push(s.value());
80 } else if meta.path.is_ident("all") {
81 meta.parse_nested_meta(|nested_meta| process_meta_item(features, &nested_meta))?;
82 } else if let Some(ident) = meta.path.get_ident() {
83 features.push(ident.to_string());
84 } else if !meta.input.peek(syn::Token![,]) {
85 let _ = meta.value()?.parse::<Lit>()?;
86 }
87 Ok(())
88}
89
90fn parse_variant_parts(attrs: &[syn::Attribute]) -> Result<(String, Vec<String>)> {
92 let mut features = Vec::new();
93 let mut docs = String::new();
94
95 for attribute in attrs {
96 match &attribute.meta {
97 syn::Meta::NameValue(nv) => add_doc_line(&mut docs, nv),
98 syn::Meta::List(l) => add_feature(&mut features, l)?,
99 _ => (),
100 }
101 }
102
103 Ok((docs, features))
104}
105
106fn get_path_type(mut path: syn::Path) -> Option<syn::Ident> {
108 let last_segment = path.segments.pop()?;
109 Some(last_segment.into_value().ident)
110}
111
112fn get_match_pair(arm: syn::Arm) -> Option<(syn::Ident, String)> {
114 let variant = match arm.pat {
115 syn::Pat::Path(path) => get_path_type(path.path)?,
116 _ => return None,
117 };
118
119 if let syn::Expr::Lit(lit) = *arm.body {
120 if let syn::Lit::Str(s) = lit.lit {
121 return Some((variant, s.value()));
122 }
123 }
124
125 None
126}
127
128fn get_metric_type(imp: &mut syn::ItemImpl) -> Option<MetricType> {
130 let (_, path, _) = imp.trait_.take()?;
131 let trait_name = get_path_type(path)?;
132
133 if trait_name == "TimerMetric" {
134 Some(MetricType::Timer)
135 } else if trait_name == "CounterMetric" {
136 Some(MetricType::Counter)
137 } else if trait_name == "HistogramMetric" {
138 Some(MetricType::Histogram)
139 } else if trait_name == "SetMetric" {
140 Some(MetricType::Set)
141 } else if trait_name == "GaugeMetric" {
142 Some(MetricType::Gauge)
143 } else {
144 None
145 }
146}
147
148fn get_type_name(ty: syn::Type) -> Option<syn::Ident> {
150 match ty {
151 syn::Type::Path(path) => get_path_type(path.path),
152 _ => None,
153 }
154}
155
156fn find_name_arms(items: Vec<syn::ImplItem>) -> Option<Vec<syn::Arm>> {
158 for item in items {
159 let method = match item {
160 syn::ImplItem::Fn(method) if method.sig.ident == "name" => method,
161 _ => continue,
162 };
163
164 for stmt in method.block.stmts {
165 if let syn::Stmt::Expr(syn::Expr::Match(mat), _) = stmt {
166 return Some(mat.arms);
167 }
168 }
169 }
170
171 None
172}
173
174fn parse_impl_parts(mut imp: syn::ItemImpl) -> Option<(MetricType, syn::Ident, Vec<syn::Arm>)> {
176 let ty = get_metric_type(&mut imp)?;
177 let type_name = get_type_name(*imp.self_ty)?;
178 let arms = find_name_arms(imp.items)?;
179 Some((ty, type_name, arms))
180}
181
182fn sort_metrics(metrics: &mut [Metric]) {
183 metrics.sort_by(|a, b| a.name.cmp(&b.name));
184}
185
186fn parse_metrics(source: &str) -> Result<Vec<Metric>> {
188 let ast = syn::parse_file(source).with_context(|| "failed to parse metrics file")?;
189
190 let mut variant_parts = HashMap::new();
191 let mut impl_parts = HashMap::new();
192
193 for item in ast.items {
194 match item {
195 syn::Item::Enum(enum_item) => {
196 for variant in enum_item.variants {
197 let path = MetricPath(enum_item.ident.clone(), variant.ident);
198 let (description, features) = parse_variant_parts(&variant.attrs)?;
199 variant_parts.insert(path, (description, features));
200 }
201 }
202 syn::Item::Impl(imp) => {
203 if let Some((ty, type_name, arms)) = parse_impl_parts(imp) {
204 for (variant, name) in arms.into_iter().filter_map(get_match_pair) {
205 let path = MetricPath(type_name.clone(), variant);
206 impl_parts.insert(path, (name, ty));
207 }
208 }
209 }
210 _ => (),
211 }
212 }
213
214 let mut metrics = Vec::with_capacity(impl_parts.len());
215
216 for (path, (name, ty)) in impl_parts {
217 let (description, features) = variant_parts.remove(&path).unwrap();
218 metrics.push(Metric {
219 ty,
220 name,
221 description,
222 features,
223 });
224 }
225
226 sort_metrics(&mut metrics);
227 Ok(metrics)
228}
229
230#[derive(Debug, Parser)]
232#[command(verbatim_doc_comment)]
233struct Cli {
234 #[arg(value_enum, short, long, default_value = "json")]
236 format: SchemaFormat,
237
238 #[arg(short, long)]
240 output: Option<PathBuf>,
241
242 #[arg(required = true)]
244 paths: Vec<PathBuf>,
245}
246
247impl Cli {
248 fn write_metrics<W: Write>(&self, writer: W, metrics: &[Metric]) -> Result<()> {
249 match self.format {
250 SchemaFormat::Json => serde_json::to_writer_pretty(writer, metrics)?,
251 SchemaFormat::Yaml => serde_yaml::to_writer(writer, metrics)?,
252 };
253
254 Ok(())
255 }
256
257 pub fn run(self) -> Result<()> {
258 let mut metrics = Vec::new();
259 for path in &self.paths {
260 metrics.extend(parse_metrics(&fs::read_to_string(path)?)?);
261 }
262 sort_metrics(&mut metrics);
263
264 match self.output {
265 Some(ref path) => self.write_metrics(File::create(path)?, &metrics)?,
266 None => self.write_metrics(io::stdout(), &metrics)?,
267 }
268
269 Ok(())
270 }
271}
272
273fn print_error(error: &anyhow::Error) {
274 eprintln!("Error: {error}");
275
276 let mut cause = error.source();
277 while let Some(ref e) = cause {
278 eprintln!(" caused by: {e}");
279 cause = e.source();
280 }
281}
282
283fn main() {
284 let cli = Cli::parse();
285
286 match cli.run() {
287 Ok(()) => (),
288 Err(error) => {
289 print_error(&error);
290 std::process::exit(1);
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_parse_metrics() -> Result<()> {
301 let source = r#"
302 /// A metric collection used for testing.
303 pub enum TestSets {
304 /// The metric we test.
305 UniqueSet,
306 /// The metric we test.
307 #[cfg(feature = "conditional")]
308 ConditionalSet,
309 /// Another metric we test.
310 #[cfg(cfg_flag)]
311 ConditionalCompileSet,
312 /// Yet another metric we test.
313 #[cfg(all(cfg_flag, feature = "conditional"))]
314 MultiConditionalCompileSet,
315 }
316
317 impl SetMetric for TestSets {
318 fn name(&self) -> &'static str {
319 match self {
320 Self::UniqueSet => "test.unique",
321 #[cfg(feature = "conditional")]
322 Self::ConditionalSet => "test.conditional",
323 #[cfg(cfg_flag)]
324 Self::ConditionalCompileSet => "test.conditional_compile",
325 #[cfg(all(cfg_flag, feature = "conditional"))]
326 Self::MultiConditionalCompileSet => "test.multi_conditional_compile"
327 }
328 }
329 }
330 "#;
331
332 let metrics = parse_metrics(source)?;
333 insta::assert_debug_snapshot!(metrics, @r#"
334 [
335 Metric {
336 ty: Set,
337 name: "test.conditional",
338 description: "The metric we test.",
339 features: [
340 "conditional",
341 ],
342 },
343 Metric {
344 ty: Set,
345 name: "test.conditional_compile",
346 description: "Another metric we test.",
347 features: [
348 "cfg_flag",
349 ],
350 },
351 Metric {
352 ty: Set,
353 name: "test.multi_conditional_compile",
354 description: "Yet another metric we test.",
355 features: [
356 "cfg_flag",
357 "conditional",
358 ],
359 },
360 Metric {
361 ty: Set,
362 name: "test.unique",
363 description: "The metric we test.",
364 features: [],
365 },
366 ]
367 "#);
368
369 Ok(())
370 }
371}