fp-macros 0.8.0

Procedural macros for generating and working with Higher-Kinded Type (HKT) traits in the fp-library crate.
Documentation
//! Documentation template builders for Kind traits.
//!
//! This module provides a builder pattern for generating documentation strings,
//! replacing direct string manipulation on `quote!()` output.

use {
	crate::{
		core::constants::macros::{
			APPLY_MACRO,
			IMPL_KIND_MACRO,
			KIND_MACRO,
			TRAIT_KIND_MACRO,
		},
		hkt::AssociatedType,
	},
	proc_macro2::Ident,
	quote::quote,
	syn::GenericParam,
};

/// Builder for Kind trait documentation.
pub struct DocumentationBuilder<'a> {
	name: &'a Ident,
	assoc_types: &'a [AssociatedType],
}

impl<'a> DocumentationBuilder<'a> {
	/// Create a new documentation builder.
	pub fn new(
		name: &'a Ident,
		assoc_types: &'a [AssociatedType],
	) -> Self {
		Self {
			name,
			assoc_types,
		}
	}

	/// Build the complete documentation string.
	pub fn build(self) -> String {
		let sections = [
			self.build_summary(),
			self.build_overview(),
			self.build_associated_types_section(),
			self.build_implementation_section(),
			self.build_naming_section(),
			self.build_see_also_section(),
		];

		sections.join("\n\n")
	}

	/// Build the summary line (first line of documentation).
	fn build_summary(&self) -> String {
		let assoc_types_summary = self
			.assoc_types
			.iter()
			.map(|assoc| {
				let ident = &assoc.signature.name;
				let generics = &assoc.signature.generics;
				let output_bounds = &assoc.signature.output_bounds;
				let output_bounds_tokens =
					if output_bounds.is_empty() { quote!() } else { quote!(: #output_bounds) };

				let s = quote!(#ident #generics #output_bounds_tokens).to_string();
				// Clean up quote! output to match requested format
				let cleaned = s
					.replace(" < ", "<")
					.replace(" >", ">")
					.replace(" , ", ", ")
					.replace(" : ", ": ");
				format!("`{cleaned}`")
			})
			.collect::<Vec<_>>()
			.join("; ");

		match self.assoc_types.len() {
			0 => "`Kind` trait.".to_string(),
			1 => format!("`Kind` with associated type: {assoc_types_summary}."),
			_ => format!("`Kind` with associated types: {assoc_types_summary}."),
		}
	}

	/// Build the overview section.
	fn build_overview(&self) -> String {
		format!(
			"Higher-Kinded Type (HKT) trait auto-generated by [`{TRAIT_KIND_MACRO}!`](crate::{TRAIT_KIND_MACRO}!), \
			representing type constructors that can be applied to generic parameters to produce \
			concrete types."
		)
		.to_string()
	}

	/// Build the associated types section.
	fn build_associated_types_section(&self) -> String {
		let mut section = String::from("# Associated Types");

		for assoc in self.assoc_types {
			section.push_str("\n\n");
			section.push_str(&self.format_assoc_type(assoc));
		}

		section
	}

	/// Format a single associated type with its details.
	fn format_assoc_type(
		&self,
		assoc: &AssociatedType,
	) -> String {
		let ident = &assoc.signature.name;

		let mut l_count = 0;
		let mut t_count = 0;
		let mut lifetimes_doc = Vec::new();
		let mut types_doc = Vec::new();

		for param in &assoc.signature.generics.params {
			match param {
				GenericParam::Lifetime(lt) => {
					l_count += 1;
					lifetimes_doc.push(format!("`{}`", lt.lifetime));
				}
				GenericParam::Type(ty) => {
					t_count += 1;
					let bounds_str = if ty.bounds.is_empty() {
						String::new()
					} else {
						let bounds = &ty.bounds;
						format!(": {}", quote!(#bounds))
					};
					types_doc.push(format!("`{}{}`", ty.ident, bounds_str));
				}
				_ => {}
			}
		}

		let output_bounds = &assoc.signature.output_bounds;
		let output_bounds_doc = if output_bounds.is_empty() {
			"None".to_string()
		} else {
			format!("`{}`", quote!(#output_bounds))
		};

		format!(
			r#"### `type {ident}`

* **Lifetimes** ({l_count}): {}
* **Type parameters** ({t_count}): {}
* **Output bounds**: {output_bounds_doc}"#,
			if l_count == 0 { "None".to_string() } else { lifetimes_doc.join(", ") },
			if t_count == 0 { "None".to_string() } else { types_doc.join(", ") }
		)
	}

	/// Build the implementation section with example code.
	fn build_implementation_section(&self) -> String {
		let impl_example_body = self
			.assoc_types
			.iter()
			.map(|assoc| {
				let ident = &assoc.signature.name;
				let generics = &assoc.signature.generics;
				let s = quote!(type #ident #generics = ConcreteType;).to_string();
				s.replace(" < ", "<")
					.replace(" >", ">")
					.replace(" , ", ", ")
					.replace(" ;", ";")
					.replace(" :", ":")
			})
			.collect::<Vec<_>>()
			.join("\n        ");

		format!(
			r#"# Implementation

To implement this trait for your type constructor, use the [`{IMPL_KIND_MACRO}!`](crate::{IMPL_KIND_MACRO}!) macro:

```ignore
{IMPL_KIND_MACRO}! {{
		  for BrandType {{
		      {impl_example_body}
		  }}
}}
```"#
		)
	}

	/// Build the naming section explaining the hash-based naming.
	fn build_naming_section(&self) -> String {
		let name = self.name;
		format!(
			r#"# Naming

The trait name `{name}` is a deterministic hash of the canonical signature, ensuring that semantically equivalent signatures always map to the same trait."#
		)
	}

	/// Build the "See Also" section with related macro links.
	fn build_see_also_section(&self) -> String {
		format!(
			r#"# See Also

* [`{KIND_MACRO}!`](crate::{KIND_MACRO}!) - Macro to generate the name of a Kind trait
* [`{IMPL_KIND_MACRO}!`](crate::{IMPL_KIND_MACRO}!) - Macro to implement a Kind trait for a brand
* [`{APPLY_MACRO}!`](crate::{APPLY_MACRO}!) - Macro to apply a Kind to generic arguments"#
		)
		.to_string()
	}
}

#[cfg(test)]
mod tests {
	use {
		super::*,
		syn::parse_quote,
	};

	#[test]
	fn test_documentation_builder_single_assoc() {
		let name: Ident = parse_quote!(Kind_12345678);
		let assoc_type: AssociatedType = parse_quote!(
			type Of<A>;
		);
		let assoc_types = vec![assoc_type];

		let builder = DocumentationBuilder::new(&name, &assoc_types);
		let doc = builder.build();

		assert!(doc.contains("`Kind` with associated type:"));
		assert!(doc.contains("# Associated Types"));
		assert!(doc.contains("### `type Of`"));
		assert!(doc.contains("# Implementation"));
		assert!(doc.contains("# Naming"));
		assert!(doc.contains("# See Also"));
	}

	#[test]
	fn test_documentation_builder_multiple_assoc() {
		let name: Ident = parse_quote!(Kind_87654321);
		let assoc1: AssociatedType = parse_quote!(
			type Of<A>;
		);
		let assoc2: AssociatedType = parse_quote!(
			type SendOf<B>: Send;
		);
		let assoc_types = vec![assoc1, assoc2];

		let builder = DocumentationBuilder::new(&name, &assoc_types);
		let doc = builder.build();

		assert!(doc.contains("`Kind` with associated types:"));
		assert!(doc.contains("### `type Of`"));
		assert!(doc.contains("### `type SendOf`"));
	}

	#[test]
	fn test_documentation_builder_with_bounds() {
		let name: Ident = parse_quote!(Kind_test);
		let assoc_type: AssociatedType = parse_quote!(
			type Of<'a, T: 'a + Clone>: Debug;
		);
		let assoc_types = vec![assoc_type];

		let builder = DocumentationBuilder::new(&name, &assoc_types);
		let doc = builder.build();

		assert!(doc.contains("**Lifetimes** (1):"));
		assert!(doc.contains("**Type parameters** (1):"));
		assert!(doc.contains("**Output bounds**:"));
	}
}