pyro_macro/format/
documentation.rs1use quote::{format_ident, quote};
2use syn::{Fields, ItemStruct, Path, Type, parse_quote};
3
4use crate::format::DocRec;
5
6pub struct MagmaDocumentation {
8 pub ident: syn::Ident,
9 pub generics: syn::Generics,
10 pub doc: Option<String>,
11 pub fields: Vec<MagmaField>,
12}
13
14pub struct MagmaField {
15 pub name: String,
16 pub ty: Type,
17 pub doc: Option<String>,
18}
19
20impl MagmaDocumentation {
21 pub fn from_item(s: &ItemStruct, doc_rec: DocRec) -> syn::Result<Self> {
23 let struct_docs = extract_docs(&s.attrs);
25
26 if struct_docs.is_none() && doc_rec.need_struct() {
28 return Err(syn::Error::new_spanned(
29 &s.ident,
30 "Client structs must have documentation (///) to generate API specs.",
31 ));
32 }
33
34 let fields = match &s.fields {
36 Fields::Named(named) => named
37 .named
38 .iter()
39 .map(|f| {
40 Ok(MagmaField {
41 name: f.ident.as_ref().unwrap().to_string(),
42 ty: f.ty.clone(),
43 doc: extract_docs(&f.attrs),
44 })
45 })
46 .collect::<syn::Result<Vec<_>>>()?,
47
48 Fields::Unnamed(unnamed) => unnamed
49 .unnamed
50 .iter()
51 .enumerate()
52 .map(|(i, f)| {
53 Ok(MagmaField {
54 name: i.to_string(),
55 ty: f.ty.clone(),
56 doc: extract_docs(&f.attrs),
57 })
58 })
59 .collect::<syn::Result<Vec<_>>>()?,
60
61 Fields::Unit => vec![],
62 };
63
64 Ok(Self {
65 ident: s.ident.clone(),
66 generics: s.generics.clone(),
67 doc: struct_docs,
68 fields,
69 })
70 }
71
72 pub fn generate(&self, import_location: &Path) -> syn::Result<proc_macro2::TokenStream> {
74 let values_path = quote! { #import_location::format::value };
75 let name = &self.ident;
76
77 let mut generics = self.generics.clone();
79 for param in &mut generics.params {
80 if let syn::GenericParam::Type(ref mut type_param) = *param {
81 type_param.bounds.push(parse_quote!(#values_path::Typeable));
82 }
83 }
84 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
85
86 let field_entries: Vec<proc_macro2::TokenStream> = self
88 .fields
89 .iter()
90 .map(|f| {
91 let fname = &f.name;
92 let fty = &f.ty;
93
94 let doc_setter = match &f.doc {
95 Some(doc_str) => {
96 quote! (.add_docstring(::std::borrow::Cow::Borrowed(#doc_str)))
97 }
98 None => quote! {},
99 };
100
101 quote! {
102 {
103 let field = #values_path::PyroField::<'static>::new(
104 #fname,
105 <#fty as #values_path::Typeable>::pyro_type(),
106 <#fty as #values_path::Typeable>::is_nullable(),
107 )#doc_setter;
108 field
109 }
110 }
111 })
112 .collect();
113
114 let struct_doc_quote = match &self.doc {
116 Some(d) => quote! { Some(::std::borrow::Cow::Borrowed(#d)) },
117 None => quote! { None },
118 };
119
120 Ok(quote! {
121 impl #impl_generics #values_path::TypeableRow for #name #ty_generics #where_clause {
122 fn schema() -> #values_path::PyroSchema<'static> {
123 #values_path::PyroSchema {
124 fields: ::std::borrow::Cow::Owned(vec![
125 #(#field_entries),*
126 ]),
127 documentation: #struct_doc_quote,
128 }
129 }
130 }
131 })
132 }
133
134 pub fn generate_for_ref(
137 &self,
138 import_location: &Path,
139 ) -> syn::Result<proc_macro2::TokenStream> {
140 let values_path = quote! { #import_location::format::value };
141 let ref_name = format_ident!("{}Ref", self.ident);
142
143 let field_entries: Vec<proc_macro2::TokenStream> = self
144 .fields
145 .iter()
146 .map(|f| {
147 let fname = &f.name;
148 let fty = &f.ty;
149
150 let doc_setter = match &f.doc {
151 Some(doc_str) => {
152 quote! (.add_docstring(::std::borrow::Cow::Borrowed(#doc_str)))
153 }
154 None => quote! {},
155 };
156
157 quote! {
158 {
159 let mut field = #values_path::PyroField::<'static>::new(
160 #fname,
161 <#fty as #values_path::Typeable>::pyro_type(),
162 <#fty as #values_path::Typeable>::is_nullable(),
163 )#doc_setter;
164 field
165 }
166 }
167 })
168 .collect();
169
170 let struct_doc_quote = match &self.doc {
171 Some(d) => quote! { Some(::std::borrow::Cow::Borrowed(#d)) },
172 None => quote! { None },
173 };
174
175 Ok(quote! {
176 impl #values_path::TypeableRow for #ref_name<'_> {
177 fn schema() -> #values_path::PyroSchema<'static> {
178 #values_path::PyroSchema {
179 fields: ::std::borrow::Cow::Owned(vec![
180 #(#field_entries),*
181 ]),
182 documentation: #struct_doc_quote,
183 }
184 }
185 }
186 })
187 }
188}
189
190pub fn generate_documented_impl(
192 item: &ItemStruct,
193 import_location: &Path,
194 doc_rec: DocRec,
195) -> syn::Result<proc_macro2::TokenStream> {
196 let documentation = MagmaDocumentation::from_item(item, doc_rec)?;
198
199 documentation.generate(import_location)
201}
202
203pub fn ref_documentation(
209 item: &ItemStruct,
210 import_location: &Path,
211 doc_rec: DocRec,
212) -> syn::Result<proc_macro2::TokenStream> {
213 let documentation = MagmaDocumentation::from_item(item, doc_rec)?;
214 documentation.generate_for_ref(import_location)
215}
216
217fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
219 let docs: Vec<String> = attrs
220 .iter()
221 .filter(|attr| attr.path().is_ident("doc"))
222 .filter_map(|attr| match &attr.meta {
223 syn::Meta::NameValue(nv) => {
224 if let syn::Expr::Lit(syn::ExprLit {
225 lit: syn::Lit::Str(lit),
226 ..
227 }) = &nv.value
228 {
229 Some(lit.value().trim().to_string())
230 } else {
231 None
232 }
233 }
234 _ => None,
235 })
236 .collect();
237
238 if docs.is_empty() {
239 None
240 } else {
241 Some(docs.join("\n"))
242 }
243}