Skip to content

Commit

Permalink
[dev-v5] Refactoring the generator of API documentation (#3322)
Browse files Browse the repository at this point in the history
  • Loading branch information
dvoituron authored Feb 9, 2025
1 parent 3ff3e4d commit ee91a05
Show file tree
Hide file tree
Showing 34 changed files with 1,481 additions and 309 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,4 @@ FodyWeavers.xsd
Microsoft.FluentUI.AspNetCore.Components.xml
/examples/Demo/FluentUI.Demo.Client/wwwroot/sources/
/examples/Demo/FluentUI.Demo.Client/wwwroot/documentation/
api-comments.json
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.9.3" />
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components.Icons" version="4.10.4" />
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components.Emoji" Version="4.6.0" />
<PackageVersion Include="LoxSmoke.DocXml" Version="3.8.0" />
<!-- Test dependencies -->
<PackageVersion Include="bunit" Version="1.33.3" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
Expand Down Expand Up @@ -53,5 +54,6 @@
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(RuntimeVersion9)" />
<PackageVersion Include="System.Text.Encodings.Web" Version="$(RuntimeVersion9)" />
<PackageVersion Include="System.Text.Json" Version="$(RuntimeVersion9)" />
<PackageVersion Include="System.IO.Pipelines" Version="$(RuntimeVersion9)" />
</ItemGroup>
</Project>
14 changes: 7 additions & 7 deletions Microsoft.FluentUI-v5.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{B98A7516
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentUI.Demo.DocViewer.Tests", "examples\Tools\FluentUI.Demo.DocViewer.Tests\FluentUI.Demo.DocViewer.Tests.csproj", "{2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentUI.Demo.Generators", "examples\Tools\FluentUI.Demo.Generators\FluentUI.Demo.Generators.csproj", "{C78BB9AD-B8CF-417B-990E-02410447649F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.SampleData", "examples\Tools\FluentUI.Demo.SampleData\FluentUI.Demo.SampleData.csproj", "{32466925-47C6-420F-B869-5F922162C3A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.SampleApi", "examples\Tools\FluentUI.Demo.SampleApi\FluentUI.Demo.SampleApi.csproj", "{E67B08B6-AEE4-4281-8700-1C87A5A3C11E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.IntegrationTests", "tests\Integration\Components.IntegrationTests.csproj", "{F380FA22-53D8-4381-B89B-4047AF544D53}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.DocApiGen", "examples\Tools\FluentUI.Demo.DocApiGen\FluentUI.Demo.DocApiGen.csproj", "{D52F6265-A983-46E0-8831-67FA80D95FBE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -75,10 +75,6 @@ Global
{2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|Any CPU.Build.0 = Release|Any CPU
{C78BB9AD-B8CF-417B-990E-02410447649F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C78BB9AD-B8CF-417B-990E-02410447649F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C78BB9AD-B8CF-417B-990E-02410447649F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C78BB9AD-B8CF-417B-990E-02410447649F}.Release|Any CPU.Build.0 = Release|Any CPU
{32466925-47C6-420F-B869-5F922162C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32466925-47C6-420F-B869-5F922162C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32466925-47C6-420F-B869-5F922162C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -90,6 +86,10 @@ Global
{F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F380FA22-53D8-4381-B89B-4047AF544D53}.Release|Any CPU.ActiveCfg = Release|AnyCPU
{F380FA22-53D8-4381-B89B-4047AF544D53}.Release|Any CPU.Build.0 = Release|AnyCPU
{D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -105,10 +105,10 @@ Global
{958BF092-4CF2-470C-B058-9244496B234F} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9}
{B98A7516-E9B2-4301-B6A3-33656BF4F4D9} = {F273876F-7528-42B3-BFE8-7CFF8ED1E2A2}
{2462C5CC-81BC-47AF-85B8-5FADD9E47ADF} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9}
{C78BB9AD-B8CF-417B-990E-02410447649F} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9}
{32466925-47C6-420F-B869-5F922162C3A7} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9}
{E67B08B6-AEE4-4281-8700-1C87A5A3C11E} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9}
{F380FA22-53D8-4381-B89B-4047AF544D53} = {A7EC98D2-21E3-4967-8C5A-D62E640305EB}
{D52F6265-A983-46E0-8831-67FA80D95FBE} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {44D95FF7-AEBE-41FB-9D40-CF1E09ADC6BC}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
<ItemGroup>
<ProjectReference Include="..\..\..\src\Core\Microsoft.FluentUI.AspNetCore.Components.csproj" />
<ProjectReference Include="..\..\Tools\FluentUI.Demo.DocViewer\FluentUI.Demo.DocViewer.csproj" />
<ProjectReference Include="..\..\Tools\FluentUI.Demo.Generators\FluentUI.Demo.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\Tools\FluentUI.Demo.SampleData\FluentUI.Demo.SampleData.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------

using System.Reflection;
using FluentUI.Demo.DocViewer;

namespace FluentUI.Demo.Client;
Expand Down Expand Up @@ -64,10 +65,54 @@ public static DemoServices AddFluentUIDemoServices(this IServiceCollection servi
options.ComponentsAssembly = typeof(Client._Imports).Assembly;
options.ResourcesAssembly = typeof(Client._Imports).Assembly;
options.ApiAssembly = typeof(Microsoft.FluentUI.AspNetCore.Components._Imports).Assembly;
options.ApiCommentSummary = (component, member) => member is null ? CodeComments.GetSummary(component.Name) : CodeComments.GetSummary(member);
options.ApiCommentSummary = (data, component, member) =>
{
if (member is null && (data is null || data?.Items?.Count <= 1))
{
return "⚠️ The file `api-comments.json` was not found. " +
"Run the project `FluentUI.Demo.DocApiGen` to generate the file. ";
}

// Component summary
if (member is null)
{
return GetMemberSummary(component.Name, "__summary__");
}

// Member summary
else
{
return GetMemberSummary(component.Name, member.GetSignature());
}

string GetMemberSummary(string sectionName, string signature)
{
if (data?.Items?.TryGetValue(sectionName, out var section) == true)
{
if (section.TryGetValue(signature, out var summary))
{
return summary;
}
}

return "";
}
};
options.SourceCodeUrl = "/sources/{0}.txt";
});

return new DemoServices(services);
}

/// <summary>
/// Get the signature of the method.
/// </summary>
/// <param name="member"></param>
/// <remarks>⚠️ Must be identical to FluentUI.Demo.DocApiGen.Extensions.ReflectionExtensions.GetSignature</remarks>
public static string GetSignature(this MemberInfo member)
{
return member.MemberType == MemberTypes.Method
? $"{member.Name}({string.Join(", ", ((MethodInfo)member).GetParameters().Select(p => p.ParameterType.Name))})"
: member.Name;
}
}
242 changes: 242 additions & 0 deletions examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// ------------------------------------------------------------------------
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------

using FluentUI.Demo.DocApiGen.Extensions;
using FluentUI.Demo.DocApiGen.Models;
using System.Globalization;
using System.Reflection;
using System.Text;

namespace FluentUI.Demo.DocApiGen;

/// <summary>
/// Engine to generate the documentation classes.
/// </summary>
public class ApiClassGenerator
{
/// <summary>
/// Initializes a new instance of the <see cref="ApiClassOptions"/> class.
/// </summary>
/// <param name="assembly"></param>
/// <param name="xmlDocumentation"></param>
public ApiClassGenerator(Assembly assembly, FileInfo xmlDocumentation)
{
Assembly = assembly;
DocXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName);
}

/// <summary>
/// Gets the assembly to generate the documentation.
/// </summary>
public Assembly Assembly { get; }

/// <summary>
/// Gets the summary reader.
/// </summary>
public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; }

/// <summary>
/// Gets the <see cref="ApiClass"/> for the specified component.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public ApiClass FromTypeName(Type type)
{
var options = new ApiClassOptions(Assembly, DocXmlReader)
{
PropertyParameterOnly = false,
};

return new ApiClass(type, options);
}

/// <summary>
/// Generates the C# code for the documentation.
/// </summary>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Not necessary")]
public string GenerateCSharp()
{
var code = new StringBuilder();
var assemblyInfo = GetAssemblyInfo(Assembly);

code.AppendLine("// ------------------------------------------------------------------------");
code.AppendLine("// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. ");
code.AppendLine("// ------------------------------------------------------------------------");
code.AppendLine();
code.AppendLine("//------------------------------------------------------------------------------");
code.AppendLine("// <auto-generated>");
code.AppendLine("// This code was generated by a tool.");
code.AppendLine("//");
code.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if");
code.AppendLine("// the code is regenerated.");
code.AppendLine("//");
code.AppendLine("// Version: " + assemblyInfo.Version + " - " + assemblyInfo.Date);
code.AppendLine("// </auto-generated>");
code.AppendLine("//------------------------------------------------------------------------------");
code.AppendLine();
code.AppendLine("using System.Reflection;");
code.AppendLine();
code.AppendLine("/// <summary />");
code.AppendLine("public class CodeComments");
code.AppendLine("{");

code.AppendLine(" /// <summary />");
code.AppendLine(" public static readonly IDictionary<string, IDictionary<string, string>> SummaryData = new Dictionary<string, IDictionary<string, string>>");
code.AppendLine(" {");

foreach (var type in Assembly.GetTypes().Where(i => i.IsValidType()))
{
var apiClass = FromTypeName(type);
var apiClassMembers = apiClass.ToDictionary();

if (apiClassMembers.Any())
{
code.AppendLine($" {{ \"{apiClass.Name}\", new Dictionary<string, string>");
code.AppendLine($" {{");
code.AppendLine($" {{ \"__summary__\", \"{FormatDescription(apiClass.Summary)}\" }},");

foreach (var member in apiClass.ToDictionary())
{
code.AppendLine($" {{ \"{member.Key}\", \"{FormatDescription(member.Value)}\" }},");
}

code.AppendLine($" }}");
code.AppendLine($" }},");
}
}

code.AppendLine(" };");
code.AppendLine();
code.AppendLine(" /// <summary />");
code.AppendLine(" public static string GetSignature(MemberInfo member)");
code.AppendLine(" {");
code.AppendLine(" return member.MemberType == MemberTypes.Method");
code.AppendLine(" ? $\"{member.Name}({string.Join(\", \", ((MethodInfo)member).GetParameters().Select(p => p.ParameterType.Name))})\"");
code.AppendLine(" : member.Name;");
code.AppendLine(" }");
code.AppendLine();
code.AppendLine(" /// <summary />");
code.AppendLine(" public static string GetSummary(MemberInfo member)");
code.AppendLine(" {");
code.AppendLine(" var name = member.Name;");
code.AppendLine(" var signature = GetSignature(member);");
code.AppendLine();
code.AppendLine(" return SummaryData.TryGetValue(name, out var comments) && comments.TryGetValue(signature, out var comment)");
code.AppendLine(" ? comment");
code.AppendLine(" : string.Empty;");
code.AppendLine(" }");
code.AppendLine("}");
code.AppendLine();

return code.ToString();
}

/// <summary>
/// Generates the JSON for the documentation.
/// </summary>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Not necessary")]
public string GenerateJson()
{
var code = new StringBuilder();
var assemblyInfo = GetAssemblyInfo(Assembly);

code.AppendLine("{");
code.AppendLine($" \"__Generated__\": {{");
code.AppendLine($" \"AssemblyVersion\": \"{assemblyInfo.Version}\",");
code.AppendLine($" \"DateUtc\": \"{assemblyInfo.Date}\"");
code.AppendLine($" }},");

foreach (var type in Assembly.GetTypes().Where(i => i.IsValidType()))
{
var apiClass = FromTypeName(type);
var apiClassMembers = apiClass.ToDictionary();

if (apiClassMembers.Any())
{
code.AppendLine($" \"{apiClass.Name}\": {{");
code.AppendLine($" \"__summary__\": \"{FormatDescription(apiClass.Summary)}\",");

foreach (var member in apiClass.ToDictionary())
{
code.AppendLine($" \"{member.Key}\": \"{FormatDescription(member.Value)}\",");
}

RemoveLastComma(code); // Remove the last comma
code.AppendLine($" }},");
}
}

RemoveLastComma(code); // Remove the last comma
code.AppendLine("}");
code.AppendLine();

return code.ToString();
}

/// <summary>
/// Saves the documentation to a file.
/// </summary>
/// <param name="fileName"></param>
/// <param name="format"></param>
public void SaveToFile(string fileName, string format)
{
if (File.Exists(fileName))
{
File.Delete(fileName);
}

if (format == "json")
{
File.WriteAllText(fileName, GenerateJson());
}
else
{
File.WriteAllText(fileName, GenerateCSharp());
}
}

/// <summary />
private static string FormatDescription(string description)
{
return description.Replace("\r\n", " ").Replace("\n", " ").Replace("\"", "\\\"");
}

/// <summary />
private static void RemoveLastComma(StringBuilder sb)
{
if (sb == null || sb.Length == 0)
{
return;
}

var lastIndex = sb.ToString().LastIndexOf(',');
sb.Remove(lastIndex, sb.Length - lastIndex);
sb.AppendLine();
}

internal static (string Version, string Date) GetAssemblyInfo(Assembly assembly)
{
// Assembly version
string strVersion = default!;
var versionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (versionAttribute != null)
{
var version = versionAttribute.InformationalVersion;
var plusIndex = version.IndexOf('+');
if (plusIndex >= 0 && plusIndex + 9 < version.Length)
{
strVersion = version[..(plusIndex + 9)];
}
else
{
strVersion = version;
}
}

// Date
return (strVersion, DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture));
}
}
Loading

0 comments on commit ee91a05

Please sign in to comment.