Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate doc-comments for structs, fields and functions #107

Merged
merged 6 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- rename `type Connection =` to `type ConnectionType =` to lessen naming conflicts
- add many doc-comments to fields and functions
- list changes to files (unchanged, modified, deleted)
- generate doc-comments for generated structs, fields and functions

## 0.0.17 (yanked)

Expand Down
44 changes: 43 additions & 1 deletion src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub struct StructField {
/// Name for the field
// TODO: should this be a Ident instead of a string?
pub name: String,
/// Actual table column name
pub column_name: String,
/// Base Rust type, like "String" or "i32" or "u32"
pub base_type: String,
/// Indicate that this field is optional
Expand Down Expand Up @@ -105,6 +107,7 @@ impl From<&ParsedColumnMacro> for StructField {
name,
base_type,
is_optional: value.is_nullable,
column_name: value.column_name.clone(),
}
}
}
Expand Down Expand Up @@ -313,11 +316,33 @@ impl<'a> Struct<'a> {
field_type = format!("Option<{}>", field_type).into();
}

lines.push(format!(
" /// Field representing column `{column_name}`",
column_name = f.column_name
));
lines.push(format!(r#" pub {field_name}: {field_type},"#));
}

let doccomment = match ty {
StructType::Read => format!(
"/// Struct representing a row in table `{table_name}`",
table_name = table.name
),
StructType::Update => format!(
"/// Update Struct for a row in table `{table_name}` for [`{read_struct}`]",
table_name = table.name,
read_struct = table.struct_name
),
StructType::Create => format!(
"/// Create Struct for a row in table `{table_name}` for [`{read_struct}`]",
table_name = table.name,
read_struct = table.struct_name
),
};

let struct_code = formatdoc!(
r#"
{doccomment}
{tsync_attr}{derive_attr}
#[diesel(table_name={table_name}{primary_key}{belongs_to})]
pub struct {struct_name}{lifetimes} {{
Expand Down Expand Up @@ -429,6 +454,7 @@ fn build_table_fns(
if create_struct.has_fields() {
buffer.push_str(&format!(
r##"
/// Insert a new row into `{table_name}` with a given [`{create_struct_identifier}`]
pub{async_keyword} fn create(db: &mut ConnectionType, item: &{create_struct_identifier}) -> QueryResult<Self> {{
use {schema_path}{table_name}::dsl::*;

Expand All @@ -439,6 +465,7 @@ fn build_table_fns(
} else {
buffer.push_str(&format!(
r##"
/// Insert a new row into `{table_name}` with all default values
pub{async_keyword} fn create(db: &mut ConnectionType) -> QueryResult<Self> {{
use {schema_path}{table_name}::dsl::*;

Expand All @@ -449,8 +476,16 @@ fn build_table_fns(
}
}

// this will also trigger for 0 primary keys, but diesel currently does not support that
let key_maybe_multiple = if primary_column_name_and_type.len() <= 1 {
"key"
} else {
"keys"
};

buffer.push_str(&format!(
r##"
/// Get a row from `{table_name}`, identified by the primary {key_maybe_multiple}
pub{async_keyword} fn read(db: &mut ConnectionType, {item_id_params}) -> QueryResult<Self> {{
use {schema_path}{table_name}::dsl::*;

Expand Down Expand Up @@ -489,6 +524,7 @@ fn build_table_fns(
// we should generate an update() method.

buffer.push_str(&format!(r##"
/// Update a row in `{table_name}`, identified by the primary {key_maybe_multiple} with [`{update_struct_identifier}`]
pub{async_keyword} fn update(db: &mut ConnectionType, {item_id_params}, item: &{update_struct_identifier}) -> QueryResult<Self> {{
use {schema_path}{table_name}::dsl::*;

Expand All @@ -500,6 +536,7 @@ fn build_table_fns(
if !is_readonly {
buffer.push_str(&format!(
r##"
/// Delete a row in `{table_name}`, identified by the primary {key_maybe_multiple}
pub{async_keyword} fn delete(db: &mut ConnectionType, {item_id_params}) -> QueryResult<usize> {{
use {schema_path}{table_name}::dsl::*;

Expand All @@ -526,13 +563,18 @@ pub fn generate_common_structs(table_options: &TableOptions<'_>) -> String {

formatdoc!(
r##"
/// Result of a `.paginate` function
{tsync}#[derive({debug_derive}, {serde_derive})]
pub struct PaginationResult<T> {{
/// Resulting items that are from the current page
pub items: Vec<T>,
/// The count of total items there are
pub total_items: i64,
/// 0-based index
/// Current page, 0-based index
pub page: i64,
/// Size of a page
pub page_size: i64,
/// Number of total possible pages, given the `page_size` and `total_items`
pub num_pages: i64,
}}
"##,
Expand Down
99 changes: 86 additions & 13 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub struct ParsedColumnMacro {
pub ty: String,
/// Rust ident for the field name
pub name: Ident,
/// Actual column name, as parsed from the attributes, or the same as "name"
pub column_name: String,
pub is_nullable: bool,
pub is_unsigned: bool,
}
Expand Down Expand Up @@ -223,19 +225,37 @@ fn handle_table_macro(
// columns group
// println!("GROUP-cols {:#?}", group);

let mut column_name: Option<Ident> = None;
// rust name parsed from the macro (the "HERE" in "HERE -> TYPE")
let mut rust_column_name: Option<Ident> = None;
// actual column name, parsed from the attribute value, if any ("#[sql_name = "test"]")
let mut actual_column_name: Option<String> = None;
let mut column_type: Option<Ident> = None;
let mut column_nullable: bool = false;
let mut column_unsigned: bool = false;
// track if the last loop was a "#" (start of a attribute)
let mut had_hashtag = false;

for column_tokens in group.stream().into_iter() {
// reset "had_hastag" but still make it available for checking
let had_hashtag_last = had_hashtag;
had_hashtag = false;
match column_tokens {
proc_macro2::TokenTree::Group(_) => {
proc_macro2::TokenTree::Group(group) => {
if had_hashtag_last {
// parse some extra information from the bracket group
// like the actual column name
if let Some((name, value)) = parse_diesel_attr_group(&group) {
if name == "sql_name" {
actual_column_name = Some(value);
}
}
}

continue;
}
proc_macro2::TokenTree::Ident(ident) => {
if column_name.is_none() {
column_name = Some(ident);
if rust_column_name.is_none() {
rust_column_name = Some(ident);
} else if ident.to_string().eq_ignore_ascii_case("Nullable") {
column_nullable = true;
} else if ident.to_string().eq_ignore_ascii_case("Unsigned") {
Expand All @@ -247,22 +267,29 @@ fn handle_table_macro(
proc_macro2::TokenTree::Punct(punct) => {
let char = punct.as_char();

if char == '#' || char == '-' || char == '>' {
if char == '#' {
had_hashtag = true;
continue;
} else if char == '-' || char == '>' {
// nothing for arrow or any additional #[]
continue;
} else if char == ','
&& column_name.is_some()
&& rust_column_name.is_some()
&& column_type.is_some()
{
// end of column def!

let rust_column_name_checked = rust_column_name.ok_or(
Error::unsupported_schema_format(
"Invalid column name syntax",
),
)?;
let column_name = actual_column_name
.unwrap_or(rust_column_name_checked.to_string());

// add the column
table_columns.push(ParsedColumnMacro {
name: column_name.ok_or(
Error::unsupported_schema_format(
"Invalid column name syntax",
),
)?,
name: rust_column_name_checked,
ty: schema_type_to_rust_type(
column_type
.ok_or(Error::unsupported_schema_format(
Expand All @@ -273,10 +300,12 @@ fn handle_table_macro(
)?,
is_nullable: column_nullable,
is_unsigned: column_unsigned,
column_name,
});

// reset the properties
column_name = None;
rust_column_name = None;
actual_column_name = None;
column_type = None;
column_unsigned = false;
column_nullable = false;
Expand All @@ -290,7 +319,7 @@ fn handle_table_macro(
}
}

if column_name.is_some()
if rust_column_name.is_some()
|| column_type.is_some()
|| column_nullable
|| column_unsigned
Expand Down Expand Up @@ -330,6 +359,50 @@ fn handle_table_macro(
})
}

/// Parse a diesel schema attribute group
/// ```rs
/// #[attr = value]
/// ```
/// into (attr, value)
fn parse_diesel_attr_group(group: &proc_macro2::Group) -> Option<(Ident, String)> {
// diesel only uses square brackets, so ignore other types
if group.delimiter() != proc_macro2::Delimiter::Bracket {
return None;
}

let mut token_stream = group.stream().into_iter();
// parse the attribute name, if it is anything else, return None
let attr_name = match token_stream.next()? {
proc_macro2::TokenTree::Ident(ident) => ident,
_ => return None,
};

// diesel always uses "=" for assignments
let punct = match token_stream.next()? {
proc_macro2::TokenTree::Punct(punct) => punct,
_ => return None,
};

if punct.as_char() != '=' {
return None;
}

// diesel print-schema only uses literals currently, if anything else is used, it should be added here
let value = match token_stream.next()? {
proc_macro2::TokenTree::Literal(literal) => literal,
_ => return None,
};

let mut value = value.to_string();

// remove the starting and ending quotes
if value.starts_with('"') && value.ends_with('"') {
value = String::from(&value[1..value.len() - 1]); // safe char boundaries because '"' is only one byte long
}

Some((attr_name, value))
}

// A function to translate diesel schema types into rust types
//
// reference: https://github.com/diesel-rs/diesel/blob/master/diesel/src/sql_types/mod.rs
Expand Down
16 changes: 15 additions & 1 deletion test/autogenerated_all/models/todos/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,48 @@ use diesel::QueryResult;

type ConnectionType = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;

/// Struct representing a row in table `todos`
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable)]
#[diesel(table_name=todos, primary_key(id))]
pub struct Todos {
/// Field representing column `id`
pub id: i32,
/// Field representing column `created_at`
pub created_at: chrono::NaiveDateTime,
}

/// Update Struct for a row in table `todos` for [`Todos`]
#[derive(Debug, Clone, Serialize, Deserialize, AsChangeset, Default)]
#[diesel(table_name=todos)]
pub struct UpdateTodos {
/// Field representing column `created_at`
pub created_at: Option<chrono::NaiveDateTime>,
}

/// Result of a `.paginate` function
#[derive(Debug, Serialize)]
pub struct PaginationResult<T> {
/// Resulting items that are from the current page
pub items: Vec<T>,
/// The count of total items there are
pub total_items: i64,
/// 0-based index
/// Current page, 0-based index
pub page: i64,
/// Size of a page
pub page_size: i64,
/// Number of total possible pages, given the `page_size` and `total_items`
pub num_pages: i64,
}

impl Todos {
/// Insert a new row into `todos` with all default values
pub fn create(db: &mut ConnectionType) -> QueryResult<Self> {
use crate::schema::todos::dsl::*;

insert_into(todos).default_values().get_result::<Self>(db)
}

/// Get a row from `todos`, identified by the primary key
pub fn read(db: &mut ConnectionType, param_id: i32) -> QueryResult<Self> {
use crate::schema::todos::dsl::*;

Expand All @@ -61,12 +73,14 @@ impl Todos {
})
}

/// Update a row in `todos`, identified by the primary key with [`UpdateTodos`]
pub fn update(db: &mut ConnectionType, param_id: i32, item: &UpdateTodos) -> QueryResult<Self> {
use crate::schema::todos::dsl::*;

diesel::update(todos.filter(id.eq(param_id))).set(item).get_result(db)
}

/// Delete a row in `todos`, identified by the primary key
pub fn delete(db: &mut ConnectionType, param_id: i32) -> QueryResult<usize> {
use crate::schema::todos::dsl::*;

Expand Down
Loading