Docs macros (#318)

* initial port

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of macro

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more macros

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* new

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of generated docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixups

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates for objects

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixiups

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* descriptions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* descriptions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove vecs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2023-08-25 13:41:04 -07:00
committed by GitHub
parent 33eb6126d4
commit 0f3f0b3b68
16 changed files with 23209 additions and 306 deletions

View File

@ -0,0 +1,23 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
convert_case = "0.6.0"
proc-macro2 = "1"
quote = "1"
serde = { version = "1.0.186", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.29", features = ["full"] }
[dev-dependencies]
expectorate = "1.0.7"
openapitor = "0.0.5"
pretty_assertions = "1.4.0"

View File

@ -0,0 +1,548 @@
// Copyright 2023 Oxide Computer Company
//! This package defines macro attributes associated with HTTP handlers. These
//! attributes are used both to define an HTTP API and to generate an OpenAPI
//! Spec (OAS) v3 document that describes the API.
// Clippy's style advice is definitely valuable, but not worth the trouble for
// automated enforcement.
#![allow(clippy::style)]
use convert_case::Casing;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use serde::Deserialize;
use serde_tokenstream::{from_tokenstream, Error};
use syn::{
parse::{Parse, ParseStream},
Attribute, Signature, Visibility,
};
#[derive(Deserialize, Debug)]
struct StdlibMetadata {
/// The name of the function in the API.
name: String,
/// Tags for the function.
#[serde(default)]
tags: Vec<String>,
/// Whether the function is unpublished.
/// Then docs will not be generated.
#[serde(default)]
unpublished: bool,
/// Whether the function is deprecated.
/// Then specific docs detailing that this is deprecated will be generated.
#[serde(default)]
deprecated: bool,
}
#[proc_macro_attribute]
pub fn stdlib(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
fn do_stdlib(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let metadata = from_tokenstream(&attr)?;
do_stdlib_inner(metadata, attr, item)
}
fn do_output(
res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>,
) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => {
let compiler_errors = errors.iter().map(|err| err.to_compile_error());
let output = quote! {
#stdlib_docs
#( #compiler_errors )*
};
output.into()
}
}
}
fn do_stdlib_inner(
metadata: StdlibMetadata,
_attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let ast: ItemFnForSignature = syn::parse2(item.clone())?;
let mut errors = Vec::new();
if ast.sig.constness.is_some() {
errors.push(Error::new_spanned(
&ast.sig.constness,
"stdlib functions may not be const functions",
));
}
if ast.sig.asyncness.is_some() {
errors.push(Error::new_spanned(
&ast.sig.fn_token,
"stdlib functions must not be async",
));
}
if ast.sig.unsafety.is_some() {
errors.push(Error::new_spanned(
&ast.sig.unsafety,
"stdlib functions may not be unsafe",
));
}
if ast.sig.abi.is_some() {
errors.push(Error::new_spanned(
&ast.sig.abi,
"stdlib functions may not use an alternate ABI",
));
}
if !ast.sig.generics.params.is_empty() {
errors.push(Error::new_spanned(
&ast.sig.generics,
"generics are not permitted for stdlib functions",
));
}
if ast.sig.variadic.is_some() {
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
}
let name = metadata.name;
let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
let name_str = name.to_string();
let fn_name = &ast.sig.ident;
let fn_name_str = fn_name.to_string().replace("inner_", "");
let fn_name_ident = format_ident!("{}", fn_name_str);
let _visibility = &ast.vis;
let (summary_text, description_text) = extract_doc_from_attrs(&ast.attrs);
let comment_text = {
let mut buf = String::new();
buf.push_str("Std lib function: ");
buf.push_str(&name_str);
if let Some(s) = &summary_text {
buf.push_str("\n");
buf.push_str(&s);
}
if let Some(s) = &description_text {
buf.push_str("\n");
buf.push_str(&s);
}
buf
};
let description_doc_comment = quote! {
#[doc = #comment_text]
};
let summary = if let Some(summary) = summary_text {
quote! { #summary }
} else {
quote! { "" }
};
let description = if let Some(description) = description_text {
quote! { #description }
} else {
quote! { "" }
};
let tags = metadata
.tags
.iter()
.map(|tag| {
quote! { #tag.to_string() }
})
.collect::<Vec<_>>();
let deprecated = if metadata.deprecated {
quote! { true }
} else {
quote! { false }
};
let unpublished = if metadata.unpublished {
quote! { true }
} else {
quote! { false }
};
let docs_crate = get_crate(None);
// When the user attaches this proc macro to a function with the wrong type
// signature, the resulting errors can be deeply inscrutable. To attempt to
// make failures easier to understand, we inject code that asserts the types
// of the various parameters. We do this by calling dummy functions that
// require a type that satisfies SharedExtractor or ExclusiveExtractor.
let mut arg_types = Vec::new();
for arg in ast.sig.inputs.iter() {
// Get the name of the argument.
let arg_name = match arg {
syn::FnArg::Receiver(pat) => {
let span = pat.self_token.span.unwrap();
span.source_text().unwrap().to_string()
}
syn::FnArg::Typed(pat) => match &*pat.pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
_ => {
errors.push(Error::new_spanned(
&pat.pat,
"stdlib functions may not use destructuring patterns",
));
continue;
}
},
};
let ty = match arg {
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let ty_string = ty
.to_string()
.replace('&', "")
.replace("mut", "")
.replace(' ', "");
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
Vec<#ty_ident>
}
} else {
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
};
let ty_string = clean_type(&ty_string);
if ty_string != "Args" {
let schema = if ty_ident.to_string().starts_with("Vec < ") {
quote! {
<#ty_ident>::json_schema(&mut generator)
}
} else {
quote! {
#ty_ident::json_schema(&mut generator)
}
};
arg_types.push(quote! {
#docs_crate::StdLibFnArg {
name: #arg_name.to_string(),
type_: #ty_string.to_string(),
schema: #schema,
required: true,
}
});
}
}
let ret_ty = ast.sig.output.clone();
let ret_ty_string = ret_ty
.into_token_stream()
.to_string()
.replace("-> ", "")
.replace("Result < ", "")
.replace(", KclError >", "");
let ret_ty_string = ret_ty_string.trim().to_string();
let ret_ty_ident = format_ident!("{}", ret_ty_string);
let ret_ty_string = clean_type(&ret_ty_string);
let return_type = quote! {
#docs_crate::StdLibFnArg {
name: "".to_string(),
type_: #ret_ty_string.to_string(),
schema: #ret_ty_ident::json_schema(&mut generator),
required: true,
}
};
// For reasons that are not well understood unused constants that use the
// (default) call_site() Span do not trigger the dead_code lint. Because
// defining but not using an endpoint is likely a programming error, we
// want to be sure to have the compiler flag this. We force this by using
// the span from the name of the function to which this macro was applied.
let span = ast.sig.ident.span();
let const_struct = quote_spanned! {span=>
pub(crate) const #name_ident: #name_ident = #name_ident {};
};
// The final TokenStream returned will have a few components that reference
// `#name_ident`, the name of the function to which this macro was applied...
let stream = quote! {
// ... a struct type called `#name_ident` that has no members
#[allow(non_camel_case_types, missing_docs)]
#description_doc_comment
pub(crate) struct #name_ident {}
// ... a constant of type `#name` whose identifier is also #name_ident
#[allow(non_upper_case_globals, missing_docs)]
#description_doc_comment
#const_struct
impl #docs_crate::StdLibFn for #name_ident
{
fn name(&self) -> String {
#name_str.to_string()
}
fn summary(&self) -> String {
#summary.to_string()
}
fn description(&self) -> String {
#description.to_string()
}
fn tags(&self) -> Vec<String> {
vec![#(#tags),*]
}
fn args(&self) -> Vec<#docs_crate::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![#(#arg_types),*]
}
fn return_value(&self) -> #docs_crate::StdLibFnArg {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
#return_type
}
fn unpublished(&self) -> bool {
#unpublished
}
fn deprecated(&self) -> bool {
#deprecated
}
fn std_lib_fn(&self) -> crate::std::StdFn {
#fn_name_ident
}
}
#item
};
// Prepend the usage message if any errors were detected.
if !errors.is_empty() {
errors.insert(0, Error::new_spanned(&ast.sig, ""));
}
Ok((stream, errors))
}
#[allow(dead_code)]
fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
quote!(#(#compile_errors)*)
}
fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
if let Some(s) = var {
if let Ok(ts) = syn::parse_str(s.as_str()) {
return ts;
}
}
quote!(crate::docs)
}
fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> (Option<String>, Option<String>) {
let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
let mut lines = attrs.iter().flat_map(|attr| {
if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
return normalize_comment_string(s.value());
}
}
}
Vec::new()
});
// Skip initial blank lines; they make for excessively terse summaries.
let summary = loop {
match lines.next() {
Some(s) if s.is_empty() => (),
next => break next,
}
};
// Skip initial blank description lines.
let first = loop {
match lines.next() {
Some(s) if s.is_empty() => (),
next => break next,
}
};
match (summary, first) {
(None, _) => (None, None),
(summary, None) => (summary, None),
(Some(summary), Some(first)) => (
Some(summary),
Some(
lines
.fold(first, |acc, comment| {
if acc.ends_with('-') || acc.ends_with('\n') || acc.is_empty() {
// Continuation lines and newlines.
format!("{}{}", acc, comment)
} else if comment.is_empty() {
// Handle fully blank comments as newlines we keep.
format!("{}\n", acc)
} else {
// Default to space-separating comment fragments.
format!("{} {}", acc, comment)
}
})
.trim_end()
.to_string(),
),
),
}
}
fn normalize_comment_string(s: String) -> Vec<String> {
s.split('\n')
.enumerate()
.map(|(idx, s)| {
// Rust-style comments are intrinsically single-line. We don't want
// to trim away formatting such as an initial '*'.
if idx == 0 {
s.trim_start().trim_end()
} else {
let trimmed = s.trim_start().trim_end();
trimmed
.strip_prefix("* ")
.unwrap_or_else(|| trimmed.strip_prefix('*').unwrap_or(trimmed))
}
})
.map(ToString::to_string)
.collect()
}
/// Represent an item without concern for its body which may (or may not)
/// contain syntax errors.
struct ItemFnForSignature {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub _block: proc_macro2::TokenStream,
}
impl Parse for ItemFnForSignature {
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
let block = input.parse()?;
Ok(ItemFnForSignature {
attrs,
vis,
sig,
_block: block,
})
}
}
fn clean_type(t: &str) -> String {
let mut t = t.to_string();
// Turn vecs into arrays.
if t.starts_with("Vec<") {
t = t.replace("Vec<", "[").replace('>', "]");
}
if t == "f64" {
return "number".to_string();
} else if t == "str" {
return "string".to_string();
} else {
return t.replace("f64", "number").to_string();
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use super::*;
#[test]
fn test_stdlib_line_to() {
let (item, errors) = do_stdlib(
quote! {
name = "lineTo",
},
quote! {
fn inner_line_to(
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
Ok(())
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/lineTo.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
}
#[test]
fn test_stdlib_min() {
let (item, errors) = do_stdlib(
quote! {
name = "min",
},
quote! {
fn inner_min(
/// The args to do shit to.
args: Vec<f64>
) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/min.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
}
}

View File

@ -0,0 +1,76 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: lineTo"]
pub(crate) struct LineTo {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: lineTo"]
pub(crate) const LineTo: LineTo = LineTo {};
impl crate::docs::StdLibFn for LineTo {
fn name(&self) -> String {
"lineTo".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![
crate::docs::StdLibFnArg {
name: "data".to_string(),
type_: "LineToData".to_string(),
schema: LineToData::json_schema(&mut generator),
required: true,
},
crate::docs::StdLibFnArg {
name: "sketch_group".to_string(),
type_: "SketchGroup".to_string(),
schema: SketchGroup::json_schema(&mut generator),
required: true,
},
]
}
fn return_value(&self) -> crate::docs::StdLibFnArg {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "SketchGroup".to_string(),
schema: SketchGroup::json_schema(&mut generator),
required: true,
}
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
line_to
}
}
fn inner_line_to(
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
Ok(())
}

View File

@ -0,0 +1,71 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: min"]
pub(crate) struct Min {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: min"]
pub(crate) const Min: Min = Min {};
impl crate::docs::StdLibFn for Min {
fn name(&self) -> String {
"min".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![crate::docs::StdLibFnArg {
name: "args".to_string(),
type_: "[number]".to_string(),
schema: <Vec<f64>>::json_schema(&mut generator),
required: true,
}]
}
fn return_value(&self) -> crate::docs::StdLibFnArg {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "number".to_string(),
schema: f64::json_schema(&mut generator),
required: true,
}
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
min
}
}
fn inner_min(#[doc = r" The args to do shit to."] args: Vec<f64>) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}