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