blob: ede234a9a64e9f66a2ef7fb91e17ca2cdd429729 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
darling::{ast, FromDeriveInput, FromField, FromMeta},
proc_macro2::TokenStream as TokenStream2,
quote::{quote, ToTokens, TokenStreamExt},
syn,
};
pub fn impl_derive_reference_doc(ast: syn::DeriveInput) -> Result<TokenStream2, syn::Error> {
let mut parsed = ReferenceDocAttributes::from_derive_input(&ast).unwrap();
let name = &parsed.ident;
let doc = get_doc_attr(&parsed.attrs);
let indent_headers = parsed
.indent_headers
.unwrap_or_else(|| doc.map(|docstr| get_last_markdown_header_depth(&docstr)).unwrap_or(0));
let generics = &ast.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
// Forward the struct-level `fields_as` value and optionally `indent_headers` to all field attributes.
match &mut parsed.data {
ast::Data::Struct(fields) => {
for field in &mut fields.fields {
field.fields_as = parsed.fields_as.clone();
match &mut field.indent_headers {
Some(_) => {}
None => {
field.indent_headers = Some(indent_headers);
}
}
}
}
ast::Data::Enum(_) => {}
}
impl ToTokens for ReferenceDocAttributes {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let mut section_tokens = quote!(
let mut s = String::new();
);
let top_level_doc_after_fields = self.top_level_doc_after_fields.unwrap_or_default();
let top_level_doc = if let Some(doc) = get_doc_attr(&self.attrs) {
format!("{}\n\n", doc)
} else {
"".to_string()
};
if !top_level_doc_after_fields {
section_tokens.append_all(quote!(s.push_str(&#top_level_doc);));
}
match &self.data {
ast::Data::Struct(fields) => {
fields.iter().for_each(|field| {
section_tokens.append_all(quote!(
s.push_str(&#field);
))
});
}
ast::Data::Enum(_) => {}
}
if top_level_doc_after_fields {
section_tokens.append_all(quote!(s.push_str("\n");));
section_tokens.append_all(quote!(s.push_str(&#top_level_doc);));
}
tokens.append_all(section_tokens);
tokens.append_all(quote!(s));
}
}
impl ToTokens for ReferenceDocFieldAttributes {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = get_ident_name(&self.ident);
let indent_headers = self.indent_headers.unwrap_or(0);
let mut rust_ty_path = expect_typepath(&self.ty);
let mut is_optional = false;
let mut is_vec = false;
if outer_type_ident_eq(&rust_ty_path, "Option") {
is_optional = true;
rust_ty_path = get_first_inner_type_from_generic(&rust_ty_path).unwrap();
}
if outer_type_ident_eq(&rust_ty_path, "Vec") {
is_vec = true;
rust_ty_path = get_first_inner_type_from_generic(&rust_ty_path).unwrap();
}
let rust_ty_string = get_outer_type_without_generics(&rust_ty_path);
// Get the json-equivalent value type for this Rust type.
let json_type_string = get_json_type_string_from_field_attrs(&self, &rust_ty_string);
match &self.fields_as {
FieldOutputType::Headings => {
let doc = get_doc_attr(&self.attrs)
.map(|s| indent_all_markdown_headers_by(&s, indent_headers + 1))
.unwrap_or_default();
let trait_output = if self.recurse {
quote!(
#rust_ty_path::get_reference_doc_markdown_with_options(#indent_headers + 1, 0)
)
} else {
quote!("")
};
let indented_format_string =
indent_markdown_header_by("# `{name}` {{#{name}}}\n\n", indent_headers);
tokens.append_all(quote!({
let doc = #doc.to_string();
let trait_output = #trait_output.to_string();
let mut output = format!(#indented_format_string, name=#name);
output.push_str("_");
if #is_vec {
output.push_str("array of ");
}
output.push_str("`");
output.push_str(#json_type_string);
output.push_str("`");
if #is_optional {
output.push_str(" (optional)");
}
output.push_str("_\n\n");
if !doc.is_empty() {
output.push_str(&doc);
output.push_str("\n\n");
}
if !trait_output.is_empty() {
output.push_str(&trait_output);
output.push_str("\n\n");
}
output
}));
}
FieldOutputType::List => {
let doc = get_doc_attr(&self.attrs)
.map(|s| indent_lines_with_spaces(&s, 2, 1))
.unwrap_or_default();
let trait_output = if self.recurse {
quote!(
#rust_ty_path::get_reference_doc_markdown_with_options(#indent_headers, 2)
)
} else {
quote!("")
};
// FieldOutputType::List
tokens.append_all(quote!({
let trait_output = #trait_output.to_string();
let mut output = format!("- `{}`: (_", #name);
if #is_optional {
output.push_str("optional ");
}
if #is_vec {
output.push_str("array of ");
}
output.push_str("`");
output.push_str(#json_type_string);
output.push_str("`_) ");
output.push_str(#doc);
if !trait_output.is_empty() {
output.push_str("\n");
output.push_str(&trait_output);
}
output.push_str("\n");
output
}));
}
}
}
}
Ok(quote! {
impl #impl_generics ::reference_doc::MarkdownReferenceDocGenerator
for #name #ty_generics #where_clause
{
fn get_reference_doc_markdown() -> String {
#parsed
}
}
})
}
/// Receiver struct for darling to parse macro arguments on a named struct.
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(reference_doc), supports(struct_named), forward_attrs(doc))]
struct ReferenceDocAttributes {
ident: syn::Ident,
data: ast::Data<(), ReferenceDocFieldAttributes>,
attrs: Vec<syn::Attribute>,
/// Instructs the doc generator to indent any markdown headers encountered
/// on fields with this many additional hash (#) marks.
///
/// A default value is derived by looking at the top-level doc comment
/// and extracting header depth.
#[darling(default)]
indent_headers: Option<usize>,
/// Instructs the doc generator to output struct fields as a list or
/// a header.
#[darling(default)]
fields_as: FieldOutputType,
/// Instructs the doc generator to place the struct's top-level doc comment
/// after the fields' doc comments.
#[darling(default)]
top_level_doc_after_fields: Option<bool>,
}
#[derive(Debug, Clone, FromMeta)]
#[darling(rename_all = "lowercase")]
enum FieldOutputType {
Headings,
List,
}
impl Default for FieldOutputType {
fn default() -> FieldOutputType {
FieldOutputType::Headings
}
}
/// Receiver struct for darling to parse macro arguments on fields in a struct.
#[derive(Debug, FromField)]
#[darling(attributes(reference_doc), forward_attrs(doc))]
struct ReferenceDocFieldAttributes {
ident: Option<syn::Ident>,
ty: syn::Type,
attrs: Vec<syn::Attribute>,
/// If specified, the JSON value type for this field. For example: "string",
/// "object", "number", "boolean".
///
/// If omitted, a naive type will be derived from the Rust type:
/// String -> string
/// bool -> boolean
/// u8, u16, i8, ... -> number
/// anything with `recursive=true` -> object
/// default -> string
#[darling(default)]
json_type: Option<String>,
/// Instructs the doc generator to retrieve markdown by calling
/// `get_reference_doc_markdown()` on the inner type of the field.
#[darling(default)]
recurse: bool,
/// Instructs the doc generator to indent any markdown headers encountered
/// with this many additional hash (#) marks.
#[darling(default)]
indent_headers: Option<usize>,
/// Forwarded from `ReferenceDocAttributes.fields_as`.
#[darling(skip)]
fields_as: FieldOutputType,
}
/// Returns true if the outer type in `p` is equal to `str`. For example, for
/// a type such as a::b::Option, this function will return true if `rhs == "Option"`.
fn outer_type_ident_eq(p: &syn::TypePath, rhs: &str) -> bool {
p.path.segments.iter().last().unwrap().ident == rhs
}
/// Extracts a TypePath from a syn::Type, and panics for anything else.
fn expect_typepath(ty: &syn::Type) -> &syn::TypePath {
match ty {
syn::Type::Path(path) => path,
_ => panic!("Not sure what to do with type: {:?}", ty),
}
}
/// Extracts the type ident string from a TypePath.
fn get_outer_type_without_generics(path: &syn::TypePath) -> String {
let segments: Vec<_> = path.path.segments.iter().map(|seg| seg.ident.to_string()).collect();
segments.join("::")
}
/// Extracts the TypePath describing the contents of an AngleBracketed type.
/// Example: Option<Vec<T>> will extract the Vec<T> portion.
fn get_first_inner_type_from_generic(path: &syn::TypePath) -> Option<&syn::TypePath> {
let args = &path.path.segments.first().unwrap().arguments;
match &args {
syn::PathArguments::AngleBracketed(angle_bracketed_args) => {
if angle_bracketed_args.args.len() > 1 {
panic!("Found multiple inner types: {:?}", args)
}
let first = angle_bracketed_args.args.first().unwrap();
match &first {
syn::GenericArgument::Type(ty) => Some(expect_typepath(ty)),
_ => panic!("No inner type found"),
}
}
syn::PathArguments::Parenthesized(_) => {
panic!("Not sure what to do with path arguments: {:?}", args)
}
syn::PathArguments::None => None,
}
}
/// Returns an appropriate JSON type string from parsed token attributes.
fn get_json_type_string_from_field_attrs(
attrs: &ReferenceDocFieldAttributes,
rust_ty_string: &str,
) -> String {
attrs.json_type.clone().unwrap_or_else(|| {
get_json_type_string_from_ty_string(&rust_ty_string)
.unwrap_or_else(|| if attrs.recurse { "object" } else { "string" })
.to_string()
})
}
/// Returns an appropriate JSON type string for a Rust type ident string.
fn get_json_type_string_from_ty_string(ty_string: &str) -> Option<&str> {
let number_types = &[
"i8", "i16", "i32", "i64", "i128", "u8", "u16", "u32", "u64", "u128", "usize", "f32", "f64",
];
let map_types = &["Map", "BTreeMap", "HashMap"];
if ty_string == "String" {
Some("string")
} else if ty_string == "bool" {
Some("bool")
} else if ty_string == "OneOrMany" {
Some("string or array of strings")
} else if map_types.iter().any(|v| v == &ty_string) {
Some("object")
} else if number_types.iter().any(|v| v == &ty_string) {
Some("number")
} else {
None
}
}
/// Extracts the field name from a `syn::Ident`.
fn get_ident_name(ident: &Option<syn::Ident>) -> String {
match &ident {
Some(val) => {
let name = val.to_string();
if name.starts_with("r#") {
name.chars().skip(2).collect()
} else {
name
}
}
None => "".to_string(),
}
}
/// Extracts the doc comments for an `syn::Attribute` and performs some
/// simple cleanup.
fn get_doc_attr(attrs: &[syn::Attribute]) -> Option<String> {
let attrs = attrs
.iter()
.filter_map(|attr| {
if !attr.path.is_ident("doc") {
return None;
}
let meta = attr.parse_meta().ok()?;
if let syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(s), .. }) = meta {
return Some(s.value());
}
None
})
.collect::<Vec<_>>();
let mut lines =
attrs.iter().flat_map(|a| a.split('\n')).map(trim_first_space).collect::<Vec<_>>();
if lines.last() == Some(&"".to_string()) {
lines.pop();
}
if lines.is_empty() {
None
} else {
Some(lines.join("\n"))
}
}
fn trim_first_space(str: &str) -> String {
if str.starts_with(" ") {
str.chars().skip(1).collect()
} else {
str.to_string()
}
}
fn get_last_markdown_header_depth(s: &str) -> usize {
let last = s.split('\n').filter(|s| s.starts_with('#')).last();
last.map(|h| h.chars().take_while(|c| c == &'#').count()).unwrap_or(0)
}
fn indent_all_markdown_headers_by(s: &str, n: usize) -> String {
if n == 0 {
s.to_string()
} else {
s.split('\n').map(|part| indent_markdown_header_by(part, n)).collect::<Vec<_>>().join("\n")
}
}
fn indent_markdown_header_by(s: &str, n: usize) -> String {
if s.starts_with("#") {
"#".to_string().repeat(n) + &s
} else {
s.to_string()
}
}
fn indent_lines_with_spaces(s: &str, n: usize, ignore_first: usize) -> String {
if n == 0 {
s.to_string()
} else {
let prefix = " ".to_string().repeat(n);
s.split('\n')
.enumerate()
.map(|(i, part)| {
if i < ignore_first || part.is_empty() {
part.to_string()
} else {
prefix.clone() + part
}
})
.collect::<Vec<_>>()
.join("\n")
}
}