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| {
68 if meta.path.is_ident("feature") {
69 let s = meta.value()?.parse::<LitStr>()?;
70 features.push(s.value());
71 } else {
72 if !meta.input.peek(syn::Token![,]) {
74 let _ = meta.value()?.parse::<Lit>()?;
75 }
76 }
77 Ok(())
78 })?;
79 }
80
81 Ok(())
82}
83
84fn parse_variant_parts(attrs: &[syn::Attribute]) -> Result<(String, Vec<String>)> {
86 let mut features = Vec::new();
87 let mut docs = String::new();
88
89 for attribute in attrs {
90 match &attribute.meta {
91 syn::Meta::NameValue(nv) => add_doc_line(&mut docs, nv),
92 syn::Meta::List(l) => add_feature(&mut features, l)?,
93 _ => (),
94 }
95 }
96
97 Ok((docs, features))
98}
99
100fn get_path_type(mut path: syn::Path) -> Option<syn::Ident> {
102 let last_segment = path.segments.pop()?;
103 Some(last_segment.into_value().ident)
104}
105
106fn get_match_pair(arm: syn::Arm) -> Option<(syn::Ident, String)> {
108 let variant = match arm.pat {
109 syn::Pat::Path(path) => get_path_type(path.path)?,
110 _ => return None,
111 };
112
113 if let syn::Expr::Lit(lit) = *arm.body {
114 if let syn::Lit::Str(s) = lit.lit {
115 return Some((variant, s.value()));
116 }
117 }
118
119 None
120}
121
122fn get_metric_type(imp: &mut syn::ItemImpl) -> Option<MetricType> {
124 let (_, path, _) = imp.trait_.take()?;
125 let trait_name = get_path_type(path)?;
126
127 if trait_name == "TimerMetric" {
128 Some(MetricType::Timer)
129 } else if trait_name == "CounterMetric" {
130 Some(MetricType::Counter)
131 } else if trait_name == "HistogramMetric" {
132 Some(MetricType::Histogram)
133 } else if trait_name == "SetMetric" {
134 Some(MetricType::Set)
135 } else if trait_name == "GaugeMetric" {
136 Some(MetricType::Gauge)
137 } else {
138 None
139 }
140}
141
142fn get_type_name(ty: syn::Type) -> Option<syn::Ident> {
144 match ty {
145 syn::Type::Path(path) => get_path_type(path.path),
146 _ => None,
147 }
148}
149
150fn find_name_arms(items: Vec<syn::ImplItem>) -> Option<Vec<syn::Arm>> {
152 for item in items {
153 let method = match item {
154 syn::ImplItem::Fn(method) if method.sig.ident == "name" => method,
155 _ => continue,
156 };
157
158 for stmt in method.block.stmts {
159 if let syn::Stmt::Expr(syn::Expr::Match(mat), _) = stmt {
160 return Some(mat.arms);
161 }
162 }
163 }
164
165 None
166}
167
168fn parse_impl_parts(mut imp: syn::ItemImpl) -> Option<(MetricType, syn::Ident, Vec<syn::Arm>)> {
170 let ty = get_metric_type(&mut imp)?;
171 let type_name = get_type_name(*imp.self_ty)?;
172 let arms = find_name_arms(imp.items)?;
173 Some((ty, type_name, arms))
174}
175
176fn sort_metrics(metrics: &mut [Metric]) {
177 metrics.sort_by(|a, b| a.name.cmp(&b.name));
178}
179
180fn parse_metrics(source: &str) -> Result<Vec<Metric>> {
182 let ast = syn::parse_file(source).with_context(|| "failed to parse metrics file")?;
183
184 let mut variant_parts = HashMap::new();
185 let mut impl_parts = HashMap::new();
186
187 for item in ast.items {
188 match item {
189 syn::Item::Enum(enum_item) => {
190 for variant in enum_item.variants {
191 let path = MetricPath(enum_item.ident.clone(), variant.ident);
192 let (description, features) = parse_variant_parts(&variant.attrs)?;
193 variant_parts.insert(path, (description, features));
194 }
195 }
196 syn::Item::Impl(imp) => {
197 if let Some((ty, type_name, arms)) = parse_impl_parts(imp) {
198 for (variant, name) in arms.into_iter().filter_map(get_match_pair) {
199 let path = MetricPath(type_name.clone(), variant);
200 impl_parts.insert(path, (name, ty));
201 }
202 }
203 }
204 _ => (),
205 }
206 }
207
208 let mut metrics = Vec::with_capacity(impl_parts.len());
209
210 for (path, (name, ty)) in impl_parts {
211 let (description, features) = variant_parts.remove(&path).unwrap();
212 metrics.push(Metric {
213 ty,
214 name,
215 description,
216 features,
217 });
218 }
219
220 sort_metrics(&mut metrics);
221 Ok(metrics)
222}
223
224#[derive(Debug, Parser)]
226#[command(verbatim_doc_comment)]
227struct Cli {
228 #[arg(value_enum, short, long, default_value = "json")]
230 format: SchemaFormat,
231
232 #[arg(short, long)]
234 output: Option<PathBuf>,
235
236 #[arg(required = true)]
238 paths: Vec<PathBuf>,
239}
240
241impl Cli {
242 fn write_metrics<W: Write>(&self, writer: W, metrics: &[Metric]) -> Result<()> {
243 match self.format {
244 SchemaFormat::Json => serde_json::to_writer_pretty(writer, metrics)?,
245 SchemaFormat::Yaml => serde_yaml::to_writer(writer, metrics)?,
246 };
247
248 Ok(())
249 }
250
251 pub fn run(self) -> Result<()> {
252 let mut metrics = Vec::new();
253 for path in &self.paths {
254 metrics.extend(parse_metrics(&fs::read_to_string(path)?)?);
255 }
256 sort_metrics(&mut metrics);
257
258 match self.output {
259 Some(ref path) => self.write_metrics(File::create(path)?, &metrics)?,
260 None => self.write_metrics(io::stdout(), &metrics)?,
261 }
262
263 Ok(())
264 }
265}
266
267fn print_error(error: &anyhow::Error) {
268 eprintln!("Error: {error}");
269
270 let mut cause = error.source();
271 while let Some(ref e) = cause {
272 eprintln!(" caused by: {e}");
273 cause = e.source();
274 }
275}
276
277fn main() {
278 let cli = Cli::parse();
279
280 match cli.run() {
281 Ok(()) => (),
282 Err(error) => {
283 print_error(&error);
284 std::process::exit(1);
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_parse_metrics() -> Result<()> {
295 let source = r#"
296 /// A metric collection used for testing.
297 pub enum TestSets {
298 /// The metric we test.
299 UniqueSet,
300 /// The metric we test.
301 #[cfg(feature = "conditional")]
302 ConditionalSet,
303 }
304
305 impl SetMetric for TestSets {
306 fn name(&self) -> &'static str {
307 match self {
308 Self::UniqueSet => "test.unique",
309 #[cfg(feature = "conditional")]
310 Self::ConditionalSet => "test.conditional",
311 }
312 }
313 }
314 "#;
315
316 let metrics = parse_metrics(source)?;
317 insta::assert_debug_snapshot!(metrics, @r#"
318 [
319 Metric {
320 ty: Set,
321 name: "test.conditional",
322 description: "The metric we test.",
323 features: [
324 "conditional",
325 ],
326 },
327 Metric {
328 ty: Set,
329 name: "test.unique",
330 description: "The metric we test.",
331 features: [],
332 },
333 ]
334 "#);
335
336 Ok(())
337 }
338}