diff --git a/Microsoft.FluentUI-v5.sln b/Microsoft.FluentUI-v5.sln index cbf9a6c60d..e419f5024a 100644 --- a/Microsoft.FluentUI-v5.sln +++ b/Microsoft.FluentUI-v5.sln @@ -43,6 +43,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{02EA EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Samples.WasmStandalone", "examples\Samples\FluentUI.Samples.WasmStandalone\FluentUI.Samples.WasmStandalone.csproj", "{EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{91F72CE5-24E4-432C-A410-360A3C9C8591}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.DocApiGen.IntegrationTests", "examples\Tools\FluentUI.Demo.DocApiGen.IntegrationTests\FluentUI.Demo.DocApiGen.IntegrationTests.csproj", "{E3369070-0374-60BB-075B-38A8D80AEA78}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.DocApiGen.Tests", "examples\Tools\FluentUI.Demo.DocApiGen.Tests\FluentUI.Demo.DocApiGen.Tests.csproj", "{8A85FC00-B688-35AF-962C-2E07C4E02ED1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,6 +104,14 @@ Global {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|Any CPU.Build.0 = Release|Any CPU + {E3369070-0374-60BB-075B-38A8D80AEA78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3369070-0374-60BB-075B-38A8D80AEA78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3369070-0374-60BB-075B-38A8D80AEA78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3369070-0374-60BB-075B-38A8D80AEA78}.Release|Any CPU.Build.0 = Release|Any CPU + {8A85FC00-B688-35AF-962C-2E07C4E02ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A85FC00-B688-35AF-962C-2E07C4E02ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A85FC00-B688-35AF-962C-2E07C4E02ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A85FC00-B688-35AF-962C-2E07C4E02ED1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -112,13 +126,16 @@ Global {0252FA12-8398-4A68-8C80-8DFBDF3021FD} = {2A11833A-E392-484A-99DF-A870C0692302} {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} + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF} = {91F72CE5-24E4-432C-A410-360A3C9C8591} {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} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {F273876F-7528-42B3-BFE8-7CFF8ED1E2A2} {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {91F72CE5-24E4-432C-A410-360A3C9C8591} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} + {E3369070-0374-60BB-075B-38A8D80AEA78} = {91F72CE5-24E4-432C-A410-360A3C9C8591} + {8A85FC00-B688-35AF-962C-2E07C4E02ED1} = {91F72CE5-24E4-432C-A410-360A3C9C8591} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {44D95FF7-AEBE-41FB-9D40-CF1E09ADC6BC} diff --git a/Microsoft.FluentUI-v5.slnx b/Microsoft.FluentUI-v5.slnx index 5cab1a9f97..bf9c49300d 100644 --- a/Microsoft.FluentUI-v5.slnx +++ b/Microsoft.FluentUI-v5.slnx @@ -7,11 +7,14 @@ - + + + + diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUI.Demo.DocApiGen.IntegrationTests.csproj b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUI.Demo.DocApiGen.IntegrationTests.csproj new file mode 100644 index 0000000000..4820151457 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUI.Demo.DocApiGen.IntegrationTests.csproj @@ -0,0 +1,45 @@ + + + + net9.0 + Exe + enable + enable + latest + false + false + + $(NoWarn);IDE0005;CA1510 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs new file mode 100644 index 0000000000..a857ae0054 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs @@ -0,0 +1,446 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using FluentUI.Demo.DocApiGen; +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Formatters; +using FluentUI.Demo.DocApiGen.Generators; +using System.Reflection; +using System.Text.Json; +using Xunit; + +namespace FluentUI.Demo.DocApiGen.IntegrationTests; + +/// +/// Integration tests using the real Microsoft.FluentUI.AspNetCore.Components.xml file. +/// These tests validate documentation generation for actual FluentUI components. +/// +public class FluentUIComponentsIntegrationTests : IDisposable +{ + private readonly FileInfo _xmlDocumentation; + private readonly string _tempOutputDirectory; + private readonly string _xmlPath; + private readonly Assembly _fluentUIAssembly; + + /// + /// Initializes a new instance of the class. + /// + public FluentUIComponentsIntegrationTests() + { + _tempOutputDirectory = Path.Combine(Path.GetTempPath(), $"DocApiGen_FluentUI_Tests_{Guid.NewGuid()}"); + Directory.CreateDirectory(_tempOutputDirectory); + + // Get project root directory + var projectRoot = GetProjectRootDirectory(); + _xmlPath = Path.Combine(projectRoot, "examples", "Tools", "FluentUI.Demo.DocApiGen", "Microsoft.FluentUI.AspNetCore.Components.xml"); + + if (!File.Exists(_xmlPath)) + { + throw new FileNotFoundException($"XML documentation file not found at: {_xmlPath}"); + } + + _xmlDocumentation = new FileInfo(_xmlPath); + + // Load the FluentUI assembly dynamically + var fluentUIAssemblyPath = Path.Combine(projectRoot, "src", "Core", "bin", "Debug", "net9.0", "Microsoft.FluentUI.AspNetCore.Components.dll"); + + if (!File.Exists(fluentUIAssemblyPath)) + { + // Try alternative path (Release build) + fluentUIAssemblyPath = Path.Combine(projectRoot, "src", "Core", "bin", "Release", "net9.0", "Microsoft.FluentUI.AspNetCore.Components.dll"); + + if (!File.Exists(fluentUIAssemblyPath)) + { + throw new FileNotFoundException($"FluentUI assembly not found. Please build the Core project first. Looked for: {fluentUIAssemblyPath}"); + } + } + + _fluentUIAssembly = Assembly.LoadFrom(fluentUIAssemblyPath); + } + + /// + /// Gets the project root directory by walking up from the current directory. + /// + private static string GetProjectRootDirectory() + { + var directory = new DirectoryInfo(Directory.GetCurrentDirectory()); + + // Look for solution file + while (directory != null) + { + var solutionFiles = directory.GetFiles("*.sln"); + if (solutionFiles.Length > 0) + { + return directory.FullName; + } + + directory = directory.Parent; + } + + throw new InvalidOperationException($"Could not find project root directory. Current directory: {Directory.GetCurrentDirectory()}"); + } + + /// + /// Cleanup temporary files and directories. + /// + public void Dispose() + { + if (Directory.Exists(_tempOutputDirectory)) + { + try + { + Directory.Delete(_tempOutputDirectory, recursive: true); + } + catch + { + // Ignore cleanup errors + } + } + } + + #region Summary Mode Tests (New Architecture) + + [Fact] + public void SummaryGenerator_ShouldGenerateJsonSuccessfully() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + + // Act + var json = generator.Generate(formatter); + + // Assert + Assert.NotNull(json); + Assert.NotEmpty(json); + Assert.Contains("__Generated__", json); + Assert.Contains("AssemblyVersion", json); + } + + [Fact] + public void SummaryGenerator_ShouldGenerateCSharpSuccessfully() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateCSharpFormatter(); + + // Act + var code = generator.Generate(formatter); + + // Assert + Assert.NotNull(code); + Assert.NotEmpty(code); + Assert.Contains("public static class CodeComments", code); + Assert.Contains("Mode: Summary", code); + Assert.Contains("SummaryData", code); + } + + [Fact] + public void SummaryGenerator_JsonOutput_ShouldContainFluentUIComponents() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + + // Act + var json = generator.Generate(formatter); + + // Assert + // Should contain common FluentUI component names + Assert.Contains("FluentButton", json); + } + + [Fact] + public void SummaryGenerator_CSharpOutput_ShouldContainFluentUIComponents() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateCSharpFormatter(); + + // Act + var code = generator.Generate(formatter); + + // Assert + // Should contain common FluentUI component names + Assert.Contains("FluentButton", code); + } + + [Fact] + public void SummaryGenerator_JsonOutput_ShouldBeValidJson() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + + // Act + var json = generator.Generate(formatter); + + // Assert - Verify it's valid JSON + var exception = Record.Exception(() => JsonDocument.Parse(json)); + Assert.Null(exception); + } + + [Fact] + public void SummaryGenerator_SaveToFile_JsonShouldSucceed() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_summary.json"); + + // Act + generator.SaveToFile(outputPath, formatter); + + // Assert + Assert.True(File.Exists(outputPath)); + + var content = File.ReadAllText(outputPath); + Assert.NotEmpty(content); + Assert.Contains("__Generated__", content); + + // Verify valid JSON + var exception = Record.Exception(() => JsonDocument.Parse(content)); + Assert.Null(exception); + } + + [Fact] + public void SummaryGenerator_SaveToFile_CSharpShouldSucceed() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateCSharpFormatter(); + var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_summary.cs"); + + // Act + generator.SaveToFile(outputPath, formatter); + + // Assert + Assert.True(File.Exists(outputPath)); + + var content = File.ReadAllText(outputPath); + Assert.NotEmpty(content); + Assert.Contains("public static class CodeComments", content); + } + + [Fact] + public void SummaryGenerator_LargeScale_ShouldCompleteWithoutErrors() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var jsonFormatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + var csharpFormatter = OutputFormatterFactory.CreateCSharpFormatter(); + + // Act & Assert - Should complete without throwing + var exception = Record.Exception(() => + { + var json = generator.Generate(jsonFormatter); + Assert.NotNull(json); + Assert.NotEmpty(json); + + var code = generator.Generate(csharpFormatter); + Assert.NotNull(code); + Assert.NotEmpty(code); + }); + + Assert.Null(exception); + } + + [Fact] + public void SummaryGenerator_OutputSize_ShouldBeReasonable() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var jsonFormatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + var csharpFormatter = OutputFormatterFactory.CreateCSharpFormatter(); + + // Act + var json = generator.Generate(jsonFormatter); + var code = generator.Generate(csharpFormatter); + + // Assert - Output should be substantial but not excessive + Assert.True(json.Length > 1000, "JSON output should be substantial"); + Assert.True(json.Length < 50_000_000, "JSON output should not be excessive"); + + Assert.True(code.Length > 1000, "C# output should be substantial"); + Assert.True(code.Length < 50_000_000, "C# output should not be excessive"); + } + + [Fact] + public void SummaryGenerator_JsonMetadata_ShouldBePresent() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + + // Act + var json = generator.Generate(formatter); + using var doc = JsonDocument.Parse(json); + + // Assert + var root = doc.RootElement; + Assert.True(root.TryGetProperty("__Generated__", out var generated)); + + Assert.True(generated.TryGetProperty("AssemblyVersion", out _)); + Assert.True(generated.TryGetProperty("DateUtc", out _)); + } + + #endregion + + #region Summary Mode Tests - Compact Format (Standard) + + [Fact] + public void SummaryGenerator_CompactFormat_ShouldGenerateCorrectStructure() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + + // Act + var json = generator.Generate(formatter); + + // Assert + Assert.NotNull(json); + Assert.NotEmpty(json); + Assert.Contains("__Generated__", json); + Assert.Contains("AssemblyVersion", json); + Assert.Contains("DateUtc", json); + Assert.Contains("FluentButton", json); + } + + [Fact] + public void SummaryGenerator_CompactFormat_ShouldBeValidJson() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + + // Act + var json = generator.Generate(formatter); + + // Assert - Verify it's valid JSON + var exception = Record.Exception(() => JsonDocument.Parse(json)); + Assert.Null(exception); + } + + [Fact] + public void SummaryGenerator_CompactFormat_SaveToFile_ShouldSucceed() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); + var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_compact.json"); + + // Act + generator.SaveToFile(outputPath, formatter); + + // Assert + Assert.True(File.Exists(outputPath)); + + var content = File.ReadAllText(outputPath); + Assert.NotEmpty(content); + Assert.Contains("__Generated__", content); + + // Verify valid JSON + var exception = Record.Exception(() => JsonDocument.Parse(content)); + Assert.Null(exception); + } + + #endregion + + #region Summary Mode Tests - Structured Format (Extended) + + [Fact] + public void SummaryGenerator_StructuredFormat_ShouldContainMetadata() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); + + // Act + var json = generator.Generate(formatter); + using var doc = JsonDocument.Parse(json); + + // Assert + var root = doc.RootElement; + Assert.True(root.TryGetProperty("metadata", out var metadata)); + Assert.True(metadata.TryGetProperty("assemblyVersion", out _)); + Assert.True(metadata.TryGetProperty("dateUtc", out _)); + Assert.True(metadata.TryGetProperty("mode", out var mode)); + Assert.Equal("Summary", mode.GetString()); + } + + #endregion + + #region All Mode Tests + + [Fact] + public void AllGenerator_ShouldGenerateJsonSuccessfully() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); + + // Act + var json = generator.Generate(formatter); + + // Assert + Assert.NotNull(json); + Assert.NotEmpty(json); + Assert.Contains("metadata", json); + Assert.Contains("components", json); + } + + [Fact] + public void AllGenerator_ShouldNotSupportCSharpFormat() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateCSharpFormatter(); + + // Act & Assert + var exception = Assert.Throws(() => generator.Generate(formatter)); + Assert.Contains("only supports JSON format", exception.Message); + } + + [Fact] + public void AllGenerator_JsonOutput_ShouldContainComponents() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); + + // Act + var json = generator.Generate(formatter); + using var doc = JsonDocument.Parse(json); + + // Assert + var root = doc.RootElement; + Assert.True(root.TryGetProperty("components", out var components)); + Assert.True(components.GetArrayLength() > 0); + } + + [Fact] + public void AllGenerator_SaveToFile_ShouldSucceed() + { + // Arrange + var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); + var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_all.json"); + + // Act + generator.SaveToFile(outputPath, formatter); + + // Assert + Assert.True(File.Exists(outputPath)); + + var content = File.ReadAllText(outputPath); + Assert.NotEmpty(content); + + // Verify valid JSON + var exception = Record.Exception(() => JsonDocument.Parse(content)); + Assert.Null(exception); + } + + #endregion +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/GlobalUsings.cs b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/GlobalUsings.cs new file mode 100644 index 0000000000..4ab42991e3 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/GlobalUsings.cs @@ -0,0 +1,7 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +global using Xunit; +global using System; +global using System.IO; diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/FluentUI.Demo.DocApiGen.Tests.csproj b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/FluentUI.Demo.DocApiGen.Tests.csproj new file mode 100644 index 0000000000..232fe53585 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/FluentUI.Demo.DocApiGen.Tests.csproj @@ -0,0 +1,35 @@ + + + + net9.0 + Exe + enable + enable + latest + true + $(NoWarn);CS1591 + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/ComponentInfoTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/ComponentInfoTests.cs new file mode 100644 index 0000000000..8e18664022 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/ComponentInfoTests.cs @@ -0,0 +1,247 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Xunit; +using FluentUI.Demo.DocApiGen.Models.AllMode; + +namespace FluentUI.Demo.DocApiGen.Tests.Models.AllMode; + +/// +/// Unit tests for . +/// +public class ComponentInfoTests +{ + [Fact] + public void Constructor_ShouldInitializeWithDefaults() + { + // Arrange & Act + var component = new ComponentInfo(); + + // Assert + Assert.Equal(string.Empty, component.Name); + Assert.Equal(string.Empty, component.FullName); + Assert.Null(component.Summary); + Assert.Null(component.Category); + Assert.False(component.IsGeneric); + Assert.Null(component.BaseClass); + Assert.Null(component.Properties); + Assert.Null(component.Events); + Assert.Null(component.Methods); + } + + [Fact] + public void Name_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo + { + // Act + Name = "FluentButton" + }; + + // Assert + Assert.Equal("FluentButton", component.Name); + } + + [Fact] + public void FullName_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo + { + // Act + FullName = "Microsoft.FluentUI.AspNetCore.Components.FluentButton" + }; + + // Assert + Assert.Equal("Microsoft.FluentUI.AspNetCore.Components.FluentButton", component.FullName); + } + + [Fact] + public void Summary_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo + { + // Act + Summary = "A button component" + }; + + // Assert + Assert.Equal("A button component", component.Summary); + } + + [Fact] + public void Category_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo + { + // Act + Category = "Forms" + }; + + // Assert + Assert.Equal("Forms", component.Category); + } + + [Fact] + public void IsGeneric_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo + { + // Act + IsGeneric = true + }; + + // Assert + Assert.True(component.IsGeneric); + } + + [Fact] + public void BaseClass_ShouldBeSettableToNull() + { + // Arrange + var component = new ComponentInfo { BaseClass = "SomeBase" }; + + // Act + component.BaseClass = null; + + // Assert + Assert.Null(component.BaseClass); + } + + [Fact] + public void BaseClass_ShouldBeSettableToValue() + { + // Arrange + var component = new ComponentInfo + { + // Act + BaseClass = "FluentComponentBase" + }; + + // Assert + Assert.Equal("FluentComponentBase", component.BaseClass); + } + + [Fact] + public void Properties_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo(); + var properties = new List + { + new() { Name = "Appearance", Type = "Appearance?" }, + new() { Name = "Disabled", Type = "bool" } + }; + + // Act + component.Properties = properties; + + // Assert + Assert.Same(properties, component.Properties); + Assert.Equal(2, component.Properties.Count); + } + + [Fact] + public void Events_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo(); + var events = new List + { + new() { Name = "OnClick", Type = "EventCallback" } + }; + + // Act + component.Events = events; + + // Assert + Assert.Same(events, component.Events); + Assert.Single(component.Events); + } + + [Fact] + public void Methods_ShouldBeSettable() + { + // Arrange + var component = new ComponentInfo(); + var methods = new List + { + new() { Name = "Focus", ReturnType = "Task" } + }; + + // Act + component.Methods = methods; + + // Assert + Assert.Same(methods, component.Methods); + Assert.Single(component.Methods); + } + + [Fact] + public void CompleteObject_ShouldBeConstructedProperly() + { + // Arrange & Act + var component = new ComponentInfo + { + Name = "FluentButton", + FullName = "Microsoft.FluentUI.AspNetCore.Components.FluentButton", + Summary = "A button component", + Category = "Forms", + IsGeneric = false, + BaseClass = "FluentComponentBase", + Properties = + [ + new PropertyInfo { Name = "Appearance", Type = "Appearance?" } + ], + Events = + [ + new EventInfo { Name = "OnClick", Type = "EventCallback" } + ], + Methods = + [ + new MethodInfo { Name = "Focus", ReturnType = "Task" } + ] + }; + + // Assert + Assert.Equal("FluentButton", component.Name); + Assert.Equal("Microsoft.FluentUI.AspNetCore.Components.FluentButton", component.FullName); + Assert.Equal("A button component", component.Summary); + Assert.Equal("Forms", component.Category); + Assert.False(component.IsGeneric); + Assert.Equal("FluentComponentBase", component.BaseClass); + Assert.Single(component.Properties); + Assert.Single(component.Events); + Assert.Single(component.Methods); + } + + [Fact] + public void Collections_CanBeModifiedAfterConstruction() + { + // Arrange + var component = new ComponentInfo + { + Properties = [], + Events = [], + Methods = [] + }; + + // Act + component.Properties.Add(new PropertyInfo { Name = "Prop1" }); + component.Events.Add(new EventInfo { Name = "Event1" }); + component.Methods.Add(new MethodInfo { Name = "Method1" }); + + // Assert + Assert.Single(component.Properties); + Assert.Single(component.Events); + Assert.Single(component.Methods); + Assert.Equal("Prop1", component.Properties[0].Name); + Assert.Equal("Event1", component.Events[0].Name); + Assert.Equal("Method1", component.Methods[0].Name); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/DocumentationMetadataTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/DocumentationMetadataTests.cs new file mode 100644 index 0000000000..f3608b6b31 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/DocumentationMetadataTests.cs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Xunit; +using FluentUI.Demo.DocApiGen.Models.AllMode; + +namespace FluentUI.Demo.DocApiGen.Tests.Models.AllMode; + +/// +/// Unit tests for . +/// +public class DocumentationMetadataTests +{ + [Fact] + public void Constructor_ShouldInitializeWithDefaults() + { + // Arrange & Act + var metadata = new DocumentationMetadata(); + + // Assert + Assert.Equal(string.Empty, metadata.AssemblyVersion); + Assert.Equal(string.Empty, metadata.GeneratedDateUtc); + Assert.Equal(0, metadata.ComponentCount); + Assert.Equal(0, metadata.EnumCount); + } + + [Fact] + public void AssemblyVersion_ShouldBeSettable() + { + // Arrange + var metadata = new DocumentationMetadata(); + + // Act + metadata.AssemblyVersion = "1.2.3"; + + // Assert + Assert.Equal("1.2.3", metadata.AssemblyVersion); + } + + [Fact] + public void GeneratedDateUtc_ShouldBeSettable() + { + // Arrange + var metadata = new DocumentationMetadata(); + var date = "2024-12-01T10:30:00Z"; + + // Act + metadata.GeneratedDateUtc = date; + + // Assert + Assert.Equal(date, metadata.GeneratedDateUtc); + } + + [Fact] + public void ComponentCount_ShouldBeSettable() + { + // Arrange + var metadata = new DocumentationMetadata(); + + // Act + metadata.ComponentCount = 42; + + // Assert + Assert.Equal(42, metadata.ComponentCount); + } + + [Fact] + public void EnumCount_ShouldBeSettable() + { + // Arrange + var metadata = new DocumentationMetadata(); + + // Act + metadata.EnumCount = 15; + + // Assert + Assert.Equal(15, metadata.EnumCount); + } + + [Fact] + public void AllProperties_CanBeSetTogether() + { + // Arrange & Act + var metadata = new DocumentationMetadata + { + AssemblyVersion = "2.0.0", + GeneratedDateUtc = "2024-12-01T12:00:00Z", + ComponentCount = 100, + EnumCount = 20 + }; + + // Assert + Assert.Equal("2.0.0", metadata.AssemblyVersion); + Assert.Equal("2024-12-01T12:00:00Z", metadata.GeneratedDateUtc); + Assert.Equal(100, metadata.ComponentCount); + Assert.Equal(20, metadata.EnumCount); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/DocumentationRootTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/DocumentationRootTests.cs new file mode 100644 index 0000000000..bfbb1d1140 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/DocumentationRootTests.cs @@ -0,0 +1,226 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Xunit; +using FluentUI.Demo.DocApiGen.Models.AllMode; + +namespace FluentUI.Demo.DocApiGen.Tests.Models.AllMode; + +/// +/// Unit tests for . +/// +public class DocumentationRootTests +{ + [Fact] + public void Constructor_ShouldInitializeWithDefaults() + { + // Arrange & Act + var root = new DocumentationRoot(); + + // Assert + Assert.NotNull(root.Metadata); + Assert.NotNull(root.Components); + Assert.NotNull(root.Enums); + Assert.Empty(root.Components); + Assert.Empty(root.Enums); + } + + [Fact] + public void Metadata_ShouldBeSettable() + { + // Arrange + var root = new DocumentationRoot(); + var metadata = new DocumentationMetadata + { + AssemblyVersion = "1.0.0", + GeneratedDateUtc = "2024-01-01T00:00:00Z", + ComponentCount = 10, + EnumCount = 5 + }; + + // Act + root.Metadata = metadata; + + // Assert + Assert.Same(metadata, root.Metadata); + Assert.Equal("1.0.0", root.Metadata.AssemblyVersion); + Assert.Equal("2024-01-01T00:00:00Z", root.Metadata.GeneratedDateUtc); + Assert.Equal(10, root.Metadata.ComponentCount); + Assert.Equal(5, root.Metadata.EnumCount); + } + + [Fact] + public void Components_ShouldBeSettable() + { + // Arrange + var root = new DocumentationRoot(); + var components = new List + { + new() { Name = "FluentButton", FullName = "Microsoft.FluentUI.AspNetCore.Components.FluentButton" }, + new() { Name = "FluentCard", FullName = "Microsoft.FluentUI.AspNetCore.Components.FluentCard" } + }; + + // Act + root.Components = components; + + // Assert + Assert.Same(components, root.Components); + Assert.Equal(2, root.Components.Count); + Assert.Equal("FluentButton", root.Components[0].Name); + Assert.Equal("FluentCard", root.Components[1].Name); + } + + [Fact] + public void Enums_ShouldBeSettable() + { + // Arrange + var root = new DocumentationRoot(); + var enums = new List + { + new() { Name = "Appearance", FullName = "Microsoft.FluentUI.AspNetCore.Components.Appearance" }, + new() { Name = "Color", FullName = "Microsoft.FluentUI.AspNetCore.Components.Color" } + }; + + // Act + root.Enums = enums; + + // Assert + Assert.Same(enums, root.Enums); + Assert.Equal(2, root.Enums.Count); + Assert.Equal("Appearance", root.Enums[0].Name); + Assert.Equal("Color", root.Enums[1].Name); + } + + [Fact] + public void CompleteObject_ShouldBeConstructedProperly() + { + // Arrange & Act + var root = new DocumentationRoot + { + Metadata = new DocumentationMetadata + { + AssemblyVersion = "2.0.0", + GeneratedDateUtc = "2024-12-01T10:30:00Z", + ComponentCount = 2, + EnumCount = 1 + }, + Components = + [ + new ComponentInfo + { + Name = "FluentButton", + FullName = "Microsoft.FluentUI.AspNetCore.Components.FluentButton", + Summary = "A button component", + Category = "Forms", + IsGeneric = false, + BaseClass = "FluentComponentBase", + Properties = + [ + new PropertyInfo + { + Name = "Appearance", + Type = "Appearance?", + Description = "The appearance of the button", + IsParameter = true + } + ], + Events = + [ + new EventInfo + { + Name = "OnClick", + Type = "EventCallback", + Description = "Triggered when button is clicked" + } + ], + Methods = + [ + new MethodInfo + { + Name = "Focus", + ReturnType = "Task", + Description = "Sets focus on the button" + } + ] + } + ], + Enums = + [ + new EnumInfo + { + Name = "Appearance", + FullName = "Microsoft.FluentUI.AspNetCore.Components.Appearance", + Description = "Defines button appearance styles", + Values = + [ + new EnumValueInfo + { + Name = "Neutral", + Value = 0, + Description = "Neutral appearance" + }, + new EnumValueInfo + { + Name = "Accent", + Value = 1, + Description = "Accent appearance" + } + ] + } + ] + }; + + // Assert + Assert.NotNull(root.Metadata); + Assert.Equal("2.0.0", root.Metadata.AssemblyVersion); + Assert.Equal(2, root.Metadata.ComponentCount); + Assert.Equal(1, root.Metadata.EnumCount); + + Assert.Single(root.Components); + Assert.Equal("FluentButton", root.Components[0].Name); + Assert.Equal("Forms", root.Components[0].Category); + Assert.NotNull(root.Components[0].Properties); + Assert.Single(root.Components[0].Properties!); + Assert.NotNull(root.Components[0].Events); + Assert.Single(root.Components[0].Events!); + Assert.NotNull(root.Components[0].Methods); + Assert.Single(root.Components[0].Methods!); + + Assert.Single(root.Enums); + Assert.Equal("Appearance", root.Enums[0].Name); + Assert.Equal(2, root.Enums[0].Values.Count); + } + + [Fact] + public void Components_CanBeModifiedAfterConstruction() + { + // Arrange + var root = new DocumentationRoot(); + + // Act + root.Components.Add(new ComponentInfo { Name = "Component1" }); + root.Components.Add(new ComponentInfo { Name = "Component2" }); + + // Assert + Assert.Equal(2, root.Components.Count); + Assert.Equal("Component1", root.Components[0].Name); + Assert.Equal("Component2", root.Components[1].Name); + } + + [Fact] + public void Enums_CanBeModifiedAfterConstruction() + { + // Arrange + var root = new DocumentationRoot(); + + // Act + root.Enums.Add(new EnumInfo { Name = "Enum1" }); + root.Enums.Add(new EnumInfo { Name = "Enum2" }); + + // Assert + Assert.Equal(2, root.Enums.Count); + Assert.Equal("Enum1", root.Enums[0].Name); + Assert.Equal("Enum2", root.Enums[1].Name); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/EnumInfoTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/EnumInfoTests.cs new file mode 100644 index 0000000000..99726b5f0b --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/AllMode/EnumInfoTests.cs @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Xunit; +using FluentUI.Demo.DocApiGen.Models.AllMode; + +namespace FluentUI.Demo.DocApiGen.Tests.Models.AllMode; + +/// +/// Unit tests for . +/// +public class EnumInfoTests +{ + [Fact] + public void Constructor_ShouldInitializeWithDefaults() + { + // Arrange & Act + var enumInfo = new EnumInfo(); + + // Assert + Assert.Equal(string.Empty, enumInfo.Name); + Assert.Equal(string.Empty, enumInfo.FullName); + Assert.Null(enumInfo.Description); + Assert.NotNull(enumInfo.Values); + Assert.Empty(enumInfo.Values); + } + + [Fact] + public void Name_ShouldBeSettable() + { + // Arrange + var enumInfo = new EnumInfo(); + + // Act + enumInfo.Name = "Appearance"; + + // Assert + Assert.Equal("Appearance", enumInfo.Name); + } + + [Fact] + public void FullName_ShouldBeSettable() + { + // Arrange + var enumInfo = new EnumInfo(); + + // Act + enumInfo.FullName = "Microsoft.FluentUI.AspNetCore.Components.Appearance"; + + // Assert + Assert.Equal("Microsoft.FluentUI.AspNetCore.Components.Appearance", enumInfo.FullName); + } + + [Fact] + public void Description_ShouldBeSettable() + { + // Arrange + var enumInfo = new EnumInfo(); + + // Act + enumInfo.Description = "Defines button appearance styles"; + + // Assert + Assert.Equal("Defines button appearance styles", enumInfo.Description); + } + + [Fact] + public void Values_ShouldBeSettable() + { + // Arrange + var enumInfo = new EnumInfo(); + var values = new List + { + new() { Name = "Neutral", Value = 0 }, + new() { Name = "Accent", Value = 1 } + }; + + // Act + enumInfo.Values = values; + + // Assert + Assert.Same(values, enumInfo.Values); + Assert.Equal(2, enumInfo.Values.Count); + } + + [Fact] + public void CompleteObject_ShouldBeConstructedProperly() + { + // Arrange & Act + var enumInfo = new EnumInfo + { + Name = "Appearance", + FullName = "Microsoft.FluentUI.AspNetCore.Components.Appearance", + Description = "Defines button appearance styles", + Values = + [ + new EnumValueInfo + { + Name = "Neutral", + Value = 0, + Description = "Neutral appearance" + }, + new EnumValueInfo + { + Name = "Accent", + Value = 1, + Description = "Accent appearance" + }, + new EnumValueInfo + { + Name = "Lightweight", + Value = 2, + Description = "Lightweight appearance" + } + ] + }; + + // Assert + Assert.Equal("Appearance", enumInfo.Name); + Assert.Equal("Microsoft.FluentUI.AspNetCore.Components.Appearance", enumInfo.FullName); + Assert.Equal("Defines button appearance styles", enumInfo.Description); + Assert.Equal(3, enumInfo.Values.Count); + Assert.Equal("Neutral", enumInfo.Values[0].Name); + Assert.Equal(0, enumInfo.Values[0].Value); + Assert.Equal("Accent", enumInfo.Values[1].Name); + Assert.Equal(1, enumInfo.Values[1].Value); + } + + [Fact] + public void Values_CanBeModifiedAfterConstruction() + { + // Arrange + var enumInfo = new EnumInfo(); + + // Act + enumInfo.Values.Add(new EnumValueInfo { Name = "Value1", Value = 0 }); + enumInfo.Values.Add(new EnumValueInfo { Name = "Value2", Value = 1 }); + + // Assert + Assert.Equal(2, enumInfo.Values.Count); + Assert.Equal("Value1", enumInfo.Values[0].Name); + Assert.Equal("Value2", enumInfo.Values[1].Name); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs new file mode 100644 index 0000000000..d6ba59d87c --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs @@ -0,0 +1,225 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Xunit; +using FluentUI.Demo.DocApiGen.Models.SummaryMode; + +namespace FluentUI.Demo.DocApiGen.Tests.Models.SummaryMode; + +/// +/// Performance tests for optimizations. +/// +public class ApiClassPerformanceTests +{ + /// + /// Test that abstract types don't cause exceptions during processing. + /// + [Fact] + public void ApiClass_ShouldHandleAbstractTypes_WithoutExceptions() + { + // Arrange + var assembly = typeof(TestAbstractClass).Assembly; + var docReader = CreateMockDocReader(); + var options = new ApiClassOptions(assembly, docReader); + + // Act + var exception = Record.Exception(() => + { + var apiClass = new ApiClass(typeof(TestAbstractClass), options); + var dictionary = apiClass.ToDictionary(); + }); + + // Assert + Assert.Null(exception); // Should not throw + } + + /// + /// Test that interface types don't cause exceptions during processing. + /// + [Fact] + public void ApiClass_ShouldHandleInterfaces_WithoutExceptions() + { + // Arrange + var assembly = typeof(ITestInterface).Assembly; + var docReader = CreateMockDocReader(); + var options = new ApiClassOptions(assembly, docReader); + + // Act + var exception = Record.Exception(() => + { + var apiClass = new ApiClass(typeof(ITestInterface), options); + var dictionary = apiClass.ToDictionary(); + }); + + // Assert + Assert.Null(exception); // Should not throw + } + + /// + /// Test that types with complex constructors don't cause exceptions. + /// + [Fact] + public void ApiClass_ShouldHandleComplexConstructors_WithoutExceptions() + { + // Arrange + var assembly = typeof(TestClassWithComplexConstructor).Assembly; + var docReader = CreateMockDocReader(); + var options = new ApiClassOptions(assembly, docReader); + + // Act + var exception = Record.Exception(() => + { + var apiClass = new ApiClass(typeof(TestClassWithComplexConstructor), options); + var dictionary = apiClass.ToDictionary(); + }); + + // Assert + Assert.Null(exception); // Should not throw + } + + /// + /// Test that concrete types with parameterless constructors work correctly. + /// + [Fact] + public void ApiClass_ShouldHandleConcreteTypes_Successfully() + { + // Arrange + var assembly = typeof(TestConcreteClass).Assembly; + var docReader = CreateMockDocReader(); + var options = new ApiClassOptions(assembly, docReader); + + // Act + var apiClass = new ApiClass(typeof(TestConcreteClass), options); + var dictionary = apiClass.ToDictionary(); + + // Assert + Assert.NotNull(dictionary); + // Should have at least the public properties + Assert.Contains("TestProperty", dictionary.Keys); + } + + /// + /// Test that processing multiple types doesn't accumulate exceptions. + /// + [Fact] + public void ApiClass_ShouldProcessMultipleTypes_Efficiently() + { + // Arrange + var assembly = typeof(TestAbstractClass).Assembly; + var docReader = CreateMockDocReader(); + var options = new ApiClassOptions(assembly, docReader); + + var types = new[] + { + typeof(TestAbstractClass), + typeof(ITestInterface), + typeof(TestConcreteClass), + typeof(TestClassWithComplexConstructor) + }; + + // Act + var startTime = DateTime.UtcNow; + var processedCount = 0; + + foreach (var type in types) + { + var exception = Record.Exception(() => + { + var apiClass = new ApiClass(type, options); + var dictionary = apiClass.ToDictionary(); + processedCount++; + }); + + Assert.Null(exception); // Should not throw + } + + var elapsed = DateTime.UtcNow - startTime; + + // Assert + Assert.Equal(types.Length, processedCount); + Assert.True(elapsed.TotalSeconds < 5, $"Processing took {elapsed.TotalSeconds} seconds, expected < 5 seconds"); + } + + /// + /// Creates a mock DocXmlReader for testing. + /// + private static LoxSmoke.DocXml.DocXmlReader CreateMockDocReader() + { + // Create a minimal XML documentation file for testing + var xmlContent = @" + + + FluentUI.Demo.DocApiGen.Tests + + + +"; + + var tempFile = Path.GetTempFileName(); + File.WriteAllText(tempFile, xmlContent); + + return new LoxSmoke.DocXml.DocXmlReader(tempFile); + } + + #region Test Types + + /// + /// Test abstract class for validation. + /// + public abstract class TestAbstractClass + { + /// + /// Test property. + /// + public string? TestProperty { get; set; } + } + + /// + /// Test interface for validation. + /// + public interface ITestInterface + { + /// + /// Test property. + /// + string? TestProperty { get; set; } + } + + /// + /// Test class with complex constructor. + /// + public class TestClassWithComplexConstructor + { + /// + /// Test property. + /// + public string? TestProperty { get; set; } + + /// + /// Constructor with required dependencies. + /// + public TestClassWithComplexConstructor(string requiredParam, int anotherParam) + { + TestProperty = requiredParam; + } + } + + /// + /// Test concrete class with parameterless constructor. + /// + public class TestConcreteClass + { + /// + /// Test property. + /// + public string? TestProperty { get; set; } + + /// + /// Test integer property. + /// + public int TestIntProperty { get; set; } + } + + #endregion +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationGenerator.cs new file mode 100644 index 0000000000..734b307394 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationGenerator.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Abstractions; + +/// +/// Defines the contract for documentation generation. +/// +public interface IDocumentationGenerator +{ + /// + /// Gets the generation mode (Summary or All). + /// + GenerationMode Mode { get; } + + /// + /// Generates documentation and returns it in the specified format. + /// + /// The output formatter to use. + /// The formatted documentation string. + string Generate(IOutputFormatter formatter); + + /// + /// Saves the generated documentation to a file. + /// + /// The output file path. + /// The output formatter to use. + void SaveToFile(string filePath, IOutputFormatter formatter); +} + +/// +/// Defines the generation mode. +/// +public enum GenerationMode +{ + /// + /// Generate summary documentation with only [Parameter] properties. + /// + Summary, + + /// + /// Generate complete documentation including all properties, methods, and events. + /// + All +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IOutputFormatter.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IOutputFormatter.cs new file mode 100644 index 0000000000..5fcf19b3cb --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IOutputFormatter.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Abstractions; + +/// +/// Defines the contract for output formatting. +/// +public interface IOutputFormatter +{ + /// + /// Gets the format name (e.g., "json", "csharp"). + /// + string FormatName { get; } + + /// + /// Formats the documentation data into a string. + /// + /// The documentation data to format. + /// The formatted string. + string Format(object data); +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs deleted file mode 100644 index e0f24b9632..0000000000 --- a/examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs +++ /dev/null @@ -1,242 +0,0 @@ -// ------------------------------------------------------------------------ -// This file is licensed to you under the MIT License. -// ------------------------------------------------------------------------ - -using FluentUI.Demo.DocApiGen.Extensions; -using FluentUI.Demo.DocApiGen.Models; -using System.Globalization; -using System.Reflection; -using System.Text; - -namespace FluentUI.Demo.DocApiGen; - -/// -/// Engine to generate the documentation classes. -/// -public class ApiClassGenerator -{ - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ApiClassGenerator(Assembly assembly, FileInfo xmlDocumentation) - { - Assembly = assembly; - DocXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); - } - - /// - /// Gets the assembly to generate the documentation. - /// - public Assembly Assembly { get; } - - /// - /// Gets the summary reader. - /// - public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } - - /// - /// Gets the for the specified component. - /// - /// - /// - public ApiClass FromTypeName(Type type) - { - var options = new ApiClassOptions(Assembly, DocXmlReader) - { - PropertyParameterOnly = false, - }; - - return new ApiClass(type, options); - } - - /// - /// Generates the C# code for the documentation. - /// - /// - [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("// This file is licensed to you under the MIT License. "); - code.AppendLine("// ------------------------------------------------------------------------"); - code.AppendLine(); - code.AppendLine("//------------------------------------------------------------------------------"); - code.AppendLine("// "); - 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("// "); - code.AppendLine("//------------------------------------------------------------------------------"); - code.AppendLine(); - code.AppendLine("using System.Reflection;"); - code.AppendLine(); - code.AppendLine("/// "); - code.AppendLine("public class CodeComments"); - code.AppendLine("{"); - - code.AppendLine(" /// "); - code.AppendLine(" public static readonly IDictionary> SummaryData = new Dictionary>"); - 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"); - 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(" /// "); - 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(" /// "); - 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(); - } - - /// - /// Generates the JSON for the documentation. - /// - /// - [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(); - } - - /// - /// Saves the documentation to a file. - /// - /// - /// - 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()); - } - } - - /// - private static string FormatDescription(string description) - { - return description.Replace("\r\n", " ").Replace("\n", " ").Replace("\"", "\\\""); - } - - /// - 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(); - 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)); - } -} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs index 37347e7749..bcb94dc561 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs @@ -43,7 +43,10 @@ public static bool IsValidType(this Type type) type.BaseType != typeof(Regex) && type.BaseType?.Name != "Icon" && type.IsAbstract == false && - type.Name.EndsWith("_g") == false; + !type.Name.EndsWith("_g") && + !type.Name.Contains('+') && // Exclude nested types + !type.IsInterface && + type.IsPublic; } /// diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/CSharpOutputFormatter.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/CSharpOutputFormatter.cs new file mode 100644 index 0000000000..6114b7c7d8 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/CSharpOutputFormatter.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text; +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Models.SummaryMode; + +namespace FluentUI.Demo.DocApiGen.Formatters; + +/// +/// Formats documentation output as C# code. +/// Only supports Summary mode data. +/// +public class CSharpOutputFormatter : IOutputFormatter +{ + /// + public string FormatName => "csharp"; + + /// + public string Format(object data) + { + ArgumentNullException.ThrowIfNull(data); + + if (data is not SummaryDocumentationData summaryData) + { + throw new InvalidOperationException( + $"CSharpOutputFormatter only supports SummaryDocumentationData. Received: {data.GetType().Name}"); + } + + return GenerateCSharpCode(summaryData); + } + + private static string GenerateCSharpCode(SummaryDocumentationData data) + { + var sb = new StringBuilder(); + + // License header + sb.AppendLine("// ------------------------------------------------------------------------"); + sb.AppendLine("// This file is licensed to you under the MIT License."); + sb.AppendLine("// ------------------------------------------------------------------------"); + sb.AppendLine(); + + // Auto-generated comment + sb.AppendLine("// "); +#pragma warning disable CA1305 // Specify IFormatProvider + sb.AppendLine($"// This code was generated by a tool on {data.Metadata.DateUtc}."); + sb.AppendLine($"// Assembly: {data.Metadata.AssemblyVersion}"); + sb.AppendLine($"// Mode: {data.Metadata.Mode}"); +#pragma warning restore CA1305 // Specify IFormatProvider + sb.AppendLine("//"); + sb.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if"); + sb.AppendLine("// the code is regenerated."); + sb.AppendLine("// "); + sb.AppendLine(); + + // Using statements + sb.AppendLine("using System.Collections.Generic;"); + sb.AppendLine("using System.Reflection;"); + sb.AppendLine(); + + // Namespace and class + sb.AppendLine("namespace FluentUI.Demo.Generated;"); + sb.AppendLine(); + sb.AppendLine("/// "); + sb.AppendLine("/// Provides access to API documentation comments."); + sb.AppendLine("/// "); + sb.AppendLine("public static class CodeComments"); + sb.AppendLine("{"); + + // Dictionary + sb.AppendLine(" /// "); + sb.AppendLine(" /// Dictionary containing all API documentation."); + sb.AppendLine(" /// "); + sb.AppendLine(" public static readonly IDictionary SummaryData = new Dictionary"); + sb.AppendLine(" {"); + + // Add entries + foreach (var entry in data.Components) + { + var key = EscapeString(entry.Key); + var summary = EscapeString(entry.Value.Summary); + var signature = EscapeString(entry.Value.Signature); + +#pragma warning disable CA1305 // Specify IFormatProvider + sb.AppendLine($" {{ \"{key}\", (\"{summary}\", \"{signature}\") }},"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + + sb.AppendLine(" };"); + sb.AppendLine(); + + // Helper methods + sb.AppendLine(" /// "); + sb.AppendLine(" /// Gets the signature for a member."); + sb.AppendLine(" /// "); + sb.AppendLine(" public static string GetSignature(MemberInfo member)"); + sb.AppendLine(" {"); + sb.AppendLine(" var key = $\"{member.DeclaringType?.FullName}.{member.Name}\";"); + sb.AppendLine(" return SummaryData.TryGetValue(key, out var data) ? data.Signature : string.Empty;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + sb.AppendLine(" /// "); + sb.AppendLine(" /// Gets the summary for a member."); + sb.AppendLine(" /// "); + sb.AppendLine(" public static string GetSummary(MemberInfo member)"); + sb.AppendLine(" {"); + sb.AppendLine(" var key = $\"{member.DeclaringType?.FullName}.{member.Name}\";"); + sb.AppendLine(" return SummaryData.TryGetValue(key, out var data) ? data.Summary : string.Empty;"); + sb.AppendLine(" }"); + + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string EscapeString(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + return value + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r") + .Replace("\t", "\\t"); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/JsonOutputFormatter.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/JsonOutputFormatter.cs new file mode 100644 index 0000000000..3136c62b2c --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/JsonOutputFormatter.cs @@ -0,0 +1,155 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Models.SummaryMode; + +namespace FluentUI.Demo.DocApiGen.Formatters; + +/// +/// Formats documentation output as JSON. +/// +public class JsonOutputFormatter : IOutputFormatter +{ + private readonly bool _indented; + private readonly bool _useCompactFormat; + + /// + /// Initializes a new instance of the class. + /// + /// Whether to indent the JSON output. + /// Whether to use the compact format for Summary mode (default true). + public JsonOutputFormatter(bool indented = true, bool useCompactFormat = true) + { + _indented = indented; + _useCompactFormat = useCompactFormat; + } + + /// + public string FormatName => "json"; + + /// + public string Format(object data) + { + ArgumentNullException.ThrowIfNull(data); + + // Si c'est un SummaryDocumentationData et qu'on veut le format compact (Summary mode standard) + if (_useCompactFormat && data is SummaryDocumentationData summaryData) + { + return FormatSummary(summaryData); + } + + // Format structuré (pour All mode ou mode étendu) + var options = new JsonSerializerOptions + { + WriteIndented = _indented, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + return JsonSerializer.Serialize(data, options); + } + + /// + /// Formats the documentation data in the Summary mode compact format. + /// This format groups members by component type with simple keys. + /// + private static string FormatSummary(SummaryDocumentationData data) + { + var sb = new StringBuilder(); + sb.AppendLine("{"); + + // Ajouter les métadonnées + sb.AppendLine(" \"__Generated__\": {"); + sb.AppendLine(CultureInfo.InvariantCulture, $" \"AssemblyVersion\": \"{EscapeJson(data.Metadata.AssemblyVersion)}\","); + sb.AppendLine(CultureInfo.InvariantCulture, $" \"DateUtc\": \"{EscapeJson(data.Metadata.DateUtc)}\""); + sb.Append(" }"); + + // Grouper les composants par type + var componentsByType = new Dictionary>(); + + foreach (var kvp in data.Components) + { + // Format de la clé: "Namespace.TypeName.__summary__" ou "Namespace.TypeName.MemberName" + var fullKey = kvp.Key; + var lastDotIndex = fullKey.LastIndexOf('.'); + + if (lastDotIndex == -1) + { + continue; + } + + var beforeLastDot = fullKey[..lastDotIndex]; + var memberName = fullKey[(lastDotIndex + 1)..]; + + // Extraire le nom du type (dernière partie avant le membre) + var typeNameStartIndex = beforeLastDot.LastIndexOf('.') + 1; + var typeName = beforeLastDot[typeNameStartIndex..]; + + if (!componentsByType.TryGetValue(typeName, out var members)) + { + members = []; + componentsByType[typeName] = members; + } + + members[memberName] = kvp.Value.Summary ?? string.Empty; + } + + // Écrire chaque type dans l'ordre + foreach (var typeEntry in componentsByType.OrderBy(x => x.Key)) + { + sb.AppendLine(","); + sb.AppendLine(CultureInfo.InvariantCulture, $" \"{EscapeJson(typeEntry.Key)}\": {{"); + + var membersList = typeEntry.Value.OrderBy(x => x.Key).ToList(); + for (var i = 0; i < membersList.Count; i++) + { + var member = membersList[i]; + var isLast = i == membersList.Count - 1; + + var escapedValue = EscapeJson(member.Value); + sb.Append(CultureInfo.InvariantCulture, $" \"{EscapeJson(member.Key)}\": \"{escapedValue}\""); + + if (!isLast) + { + sb.AppendLine(","); + } + else + { + sb.AppendLine(); + } + } + + sb.Append(" }"); + } + + sb.AppendLine(); + sb.AppendLine("}"); + + return sb.ToString(); + } + + /// + /// Escapes special characters for JSON strings. + /// + private static string EscapeJson(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + return value + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r\n", " ") + .Replace("\n", " ") + .Replace("\r", " "); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/OutputFormatterFactory.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/OutputFormatterFactory.cs new file mode 100644 index 0000000000..c537fd6da8 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Formatters/OutputFormatterFactory.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using FluentUI.Demo.DocApiGen.Abstractions; + +namespace FluentUI.Demo.DocApiGen.Formatters; + +/// +/// Factory for creating output formatters. +/// +public static class OutputFormatterFactory +{ + /// + /// Creates an output formatter for the specified format name. + /// + /// The format name ("json" or "csharp"). + /// Whether to indent the output (JSON only). + /// Whether to use compact format for Summary mode (default true). + /// An output formatter instance. + /// Thrown when formatName is null or empty. + /// Thrown when the format is not supported. + public static IOutputFormatter Create(string formatName, bool indented = true, bool useCompactFormat = true) + { + if (string.IsNullOrWhiteSpace(formatName)) + { + throw new ArgumentException("Format name cannot be null or empty.", nameof(formatName)); + } + + return formatName.ToLowerInvariant() switch + { + "json" => new JsonOutputFormatter(indented, useCompactFormat), + "csharp" or "cs" => new CSharpOutputFormatter(), + _ => throw new NotSupportedException($"Output format '{formatName}' is not supported. Supported formats: json, csharp.") + }; + } + + /// + /// Creates a JSON output formatter. + /// + /// Whether to indent the JSON output. + /// Whether to use compact format for Summary mode (default true). + /// A JSON output formatter. + public static IOutputFormatter CreateJsonFormatter(bool indented = true, bool useCompactFormat = true) + { + return new JsonOutputFormatter(indented, useCompactFormat); + } + + /// + /// Creates a C# output formatter. + /// + /// A C# output formatter. + public static IOutputFormatter CreateCSharpFormatter() + { + return new CSharpOutputFormatter(); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs new file mode 100644 index 0000000000..b9bace2286 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs @@ -0,0 +1,337 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using System.Reflection; +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models.AllMode; +using FluentUI.Demo.DocApiGen.Models.SummaryMode; + +namespace FluentUI.Demo.DocApiGen.Generators; + +/// +/// Generates All mode documentation (complete: all properties, methods, events). +/// Supports JSON output format only. +/// +public sealed class AllDocumentationGenerator : DocumentationGeneratorBase +{ + private readonly LoxSmoke.DocXml.DocXmlReader _docXmlReader; + + /// + /// Initializes a new instance of the class. + /// + /// The assembly to generate documentation for. + /// The XML documentation file. + public AllDocumentationGenerator(Assembly assembly, FileInfo xmlDocumentation) + : base(assembly, xmlDocumentation) + { + _docXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + } + + /// + public override GenerationMode Mode => GenerationMode.All; + + /// + public override string Generate(IOutputFormatter formatter) + { + ArgumentNullException.ThrowIfNull(formatter); + + if (formatter.FormatName != "json") + { + throw new NotSupportedException( + $"AllDocumentationGenerator only supports JSON format. Requested format: {formatter.FormatName}"); + } + + var data = BuildDocumentationData(); + return formatter.Format(data); + } + + /// + /// Builds the complete documentation data structure. + /// + private DocumentationRoot BuildDocumentationData() + { + var (version, date) = GetAssemblyInfo(); + var components = new List(); + var enums = new List(); + + var validTypes = Assembly.GetTypes().Where(IsValidComponentType).ToList(); + var enumTypes = Assembly.GetTypes().Where(t => t.IsEnum && t.IsPublic).ToList(); + + Console.WriteLine($"Processing {validTypes.Count} components and {enumTypes.Count} enums..."); + + // Generate components + var componentCount = 0; + foreach (var type in validTypes) + { + componentCount++; + if (componentCount % 10 == 0) + { + Console.Write($"\rProcessed {componentCount}/{validTypes.Count} components..."); + } + + var componentInfo = GenerateComponentInfo(type); + if (componentInfo != null) + { + components.Add(componentInfo); + } + } + + Console.WriteLine(); + + // Generate enums + var enumCount = 0; + foreach (var type in enumTypes) + { + enumCount++; + enums.Add(GenerateEnumInfo(type)); + } + + Console.WriteLine($"✓ Processed {validTypes.Count} components and {enumTypes.Count} enums."); + + return new DocumentationRoot + { + Metadata = new DocumentationMetadata + { + AssemblyVersion = version, + GeneratedDateUtc = date, + ComponentCount = components.Count, + EnumCount = enums.Count + }, + Components = components, + Enums = enums + }; + } + + /// + /// Generates component information for a specific type. + /// + private ComponentInfo? GenerateComponentInfo(Type type) + { + try + { + var options = new ApiClassOptions(Assembly, _docXmlReader) + { + Mode = GenerationMode.All + }; + + var apiClass = new ApiClass(type, options); + + var component = new ComponentInfo + { + Name = apiClass.Name, + FullName = type.FullName ?? type.Name, + Summary = !string.IsNullOrWhiteSpace(apiClass.Summary) ? apiClass.Summary : null, + Category = DetermineCategory(type), + IsGeneric = type.IsGenericType, + BaseClass = type.BaseType?.Name + }; + + // Extract properties - only if we have any + var properties = new List(); + foreach (var property in apiClass.Properties) + { + var isInherited = property.MemberInfo.DeclaringType != type; + var description = !string.IsNullOrWhiteSpace(property.Description) ? property.Description : null; + + properties.Add(new Models.AllMode.PropertyInfo + { + Name = property.Name, + Type = property.Type, + Description = description, + IsParameter = property.IsParameter, + IsInherited = isInherited, + DefaultValue = property.Default, + EnumValues = property.EnumValues != null && property.EnumValues.Length > 0 ? property.EnumValues : null + }); + } + + component.Properties = properties.Count > 0 ? properties : null; + + // Extract events - only if we have any + var events = new List(); + foreach (var evt in apiClass.Events) + { + var isInherited = evt.MemberInfo.DeclaringType != type; + var description = !string.IsNullOrWhiteSpace(evt.Description) ? evt.Description : null; + + events.Add(new Models.AllMode.EventInfo + { + Name = evt.Name, + Type = evt.Type, + Description = description, + IsInherited = isInherited + }); + } + + component.Events = events.Count > 0 ? events : null; + + // Extract methods - only if we have any + var methods = new List(); + foreach (var method in apiClass.Methods) + { + var isInherited = method.MemberInfo.DeclaringType != type; + var description = !string.IsNullOrWhiteSpace(method.Description) ? method.Description : null; + + methods.Add(new Models.AllMode.MethodInfo + { + Name = method.Name, + ReturnType = method.Type, + Description = description, + Parameters = method.Parameters != null && method.Parameters.Length > 0 ? method.Parameters : null, + IsInherited = isInherited + }); + } + + component.Methods = methods.Count > 0 ? methods : null; + + return component; + } + catch (Exception ex) + { + Console.WriteLine(); + Console.WriteLine($"[WARNING] Error processing component {type.FullName}: {ex.Message}"); + return null; + } + } + + /// + /// Generates enum information for a specific type. + /// + private EnumInfo GenerateEnumInfo(Type type) + { + var values = new List(); + var names = Enum.GetNames(type); + var enumValues = Enum.GetValues(type); + + for (var i = 0; i < names.Length; i++) + { + var name = names[i]; + var value = Convert.ToInt32(enumValues.GetValue(i), CultureInfo.InvariantCulture); + var field = type.GetField(name); + var description = field != null ? _docXmlReader.GetMemberSummary(field) : string.Empty; + + values.Add(new EnumValueInfo + { + Name = name, + Value = value, + Description = !string.IsNullOrWhiteSpace(description) ? description : null + }); + } + + var enumDescription = _docXmlReader.GetComponentSummary(type); + + return new EnumInfo + { + Name = type.Name, + FullName = type.FullName ?? type.Name, + Description = !string.IsNullOrWhiteSpace(enumDescription) ? enumDescription : null, + Values = values + }; + } + + /// + /// Checks if a type is a valid component type. + /// + private static bool IsValidComponentType(Type type) + { + return type != null && + type.IsPublic && + !type.IsAbstract && + !type.IsInterface && + type.IsClass && + !Constants.EXCLUDE_TYPES.Contains(type.Name) && + !type.Name.Contains('<') && + !type.Name.Contains('>') && + !type.Name.EndsWith("_g", StringComparison.Ordinal) && + type.IsValidType(); + } + + /// + /// Determines the category of a component based on its name. + /// + private static string DetermineCategory(Type type) + { + var name = type.Name; + + if (name.Contains("Button", StringComparison.OrdinalIgnoreCase)) + { + return "Button"; + } + + if (name.Contains("Input", StringComparison.OrdinalIgnoreCase) || + name.Contains("TextField", StringComparison.OrdinalIgnoreCase)) + { + return "Input"; + } + + if (name.Contains("Dialog", StringComparison.OrdinalIgnoreCase)) + { + return "Dialog"; + } + + if (name.Contains("Menu", StringComparison.OrdinalIgnoreCase)) + { + return "Menu"; + } + + if (name.Contains("Nav", StringComparison.OrdinalIgnoreCase)) + { + return "Navigation"; + } + + if (name.Contains("Grid", StringComparison.OrdinalIgnoreCase) || + name.Contains("Table", StringComparison.OrdinalIgnoreCase)) + { + return "DataGrid"; + } + + if (name.Contains("Card", StringComparison.OrdinalIgnoreCase)) + { + return "Card"; + } + + if (name.Contains("Icon", StringComparison.OrdinalIgnoreCase)) + { + return "Icon"; + } + + if (name.Contains("Layout", StringComparison.OrdinalIgnoreCase) || + name.Contains("Stack", StringComparison.OrdinalIgnoreCase)) + { + return "Layout"; + } + + return "Components"; + } + + /// + /// Gets assembly version and current date. + /// + private (string Version, string Date) GetAssemblyInfo() + { + var version = "Unknown"; + + var versionAttribute = Assembly.GetCustomAttribute(); + if (versionAttribute != null) + { + var versionString = versionAttribute.InformationalVersion; + var plusIndex = versionString.IndexOf('+'); + + if (plusIndex >= 0 && plusIndex + 9 < versionString.Length) + { + version = versionString[..(plusIndex + 9)]; + } + else + { + version = versionString; + } + } + + var date = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); + + return (version, date); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs new file mode 100644 index 0000000000..3fa2a1a780 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using FluentUI.Demo.DocApiGen.Abstractions; + +namespace FluentUI.Demo.DocApiGen.Generators; + +/// +/// Base class for documentation generators implementing common functionality. +/// +public abstract class DocumentationGeneratorBase : IDocumentationGenerator +{ + /// + /// Represents the assembly associated with the current context or operation. + /// + protected readonly Assembly Assembly; + + /// + /// Represents the XML documentation file associated with the assembly. + /// + protected readonly FileInfo XmlDocumentation; + + /// + /// Initializes a new instance of the class. + /// + /// The assembly to generate documentation for. + /// The XML documentation file. + protected DocumentationGeneratorBase(Assembly assembly, FileInfo xmlDocumentation) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + XmlDocumentation = xmlDocumentation ?? throw new ArgumentNullException(nameof(xmlDocumentation)); + + if (!xmlDocumentation.Exists) + { + throw new FileNotFoundException($"XML documentation file not found: {xmlDocumentation.FullName}"); + } + } + + /// + public abstract GenerationMode Mode { get; } + + /// + public abstract string Generate(IOutputFormatter formatter); + + /// + public virtual void SaveToFile(string filePath, IOutputFormatter formatter) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentException("File path cannot be null or empty.", nameof(filePath)); + } + + ArgumentNullException.ThrowIfNull(formatter); + + var output = Generate(formatter); + + // Delete existing file if it exists + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + // Ensure directory exists + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(filePath, output); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs new file mode 100644 index 0000000000..22db0b3b6c --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using FluentUI.Demo.DocApiGen.Abstractions; + +namespace FluentUI.Demo.DocApiGen.Generators; + +/// +/// Factory for creating documentation generators. +/// +public static class DocumentationGeneratorFactory +{ + /// + /// Creates a documentation generator for the specified mode. + /// + /// The generation mode. + /// The assembly to generate documentation for. + /// The XML documentation file. + /// A documentation generator instance. + /// Thrown when assembly or xmlDocumentation is null. + /// Thrown when the mode is not supported. + public static IDocumentationGenerator Create( + GenerationMode mode, + Assembly assembly, + FileInfo xmlDocumentation) + { + ArgumentNullException.ThrowIfNull(assembly); + + ArgumentNullException.ThrowIfNull(xmlDocumentation); + + return mode switch + { + GenerationMode.Summary => new SummaryDocumentationGenerator(assembly, xmlDocumentation), + GenerationMode.All => new AllDocumentationGenerator(assembly, xmlDocumentation), + _ => throw new NotSupportedException($"Generation mode '{mode}' is not supported.") + }; + } + + /// + /// Creates a Summary mode documentation generator. + /// + /// The assembly to generate documentation for. + /// The XML documentation file. + /// A Summary mode documentation generator. + public static IDocumentationGenerator CreateSummaryGenerator( + Assembly assembly, + FileInfo xmlDocumentation) + { + return Create(GenerationMode.Summary, assembly, xmlDocumentation); + } + + /// + /// Creates an All mode documentation generator. + /// + /// The assembly to generate documentation for. + /// The XML documentation file. + /// An All mode documentation generator. + public static IDocumentationGenerator CreateAllGenerator( + Assembly assembly, + FileInfo xmlDocumentation) + { + return Create(GenerationMode.All, assembly, xmlDocumentation); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs new file mode 100644 index 0000000000..b01c112d3c --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using System.Reflection; +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models.SummaryMode; + +namespace FluentUI.Demo.DocApiGen.Generators; + +/// +/// Generates Summary mode documentation (only [Parameter] properties). +/// Supports JSON and C# output formats. +/// +public sealed class SummaryDocumentationGenerator : DocumentationGeneratorBase +{ + private readonly LoxSmoke.DocXml.DocXmlReader _docXmlReader; + + /// + /// Initializes a new instance of the class. + /// + /// The assembly to generate documentation for. + /// The XML documentation file. + public SummaryDocumentationGenerator(Assembly assembly, FileInfo xmlDocumentation) + : base(assembly, xmlDocumentation) + { + _docXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + } + + /// + public override GenerationMode Mode => GenerationMode.Summary; + + /// + public override string Generate(IOutputFormatter formatter) + { + ArgumentNullException.ThrowIfNull(formatter); + + var data = BuildDocumentationData(); + return formatter.Format(data); + } + + /// + /// Builds the documentation data structure. + /// + private SummaryDocumentationData BuildDocumentationData() + { + var (version, date) = GetAssemblyInfo(); + var components = new Dictionary(); + + var options = new ApiClassOptions(Assembly, _docXmlReader) + { + Mode = GenerationMode.Summary + }; + + var validTypes = Assembly.GetTypes().Where(t => t.IsValidType()).ToList(); + Console.WriteLine($"Processing {validTypes.Count} valid types..."); + + var processedCount = 0; + foreach (var type in validTypes) + { + processedCount++; + if (processedCount % 10 == 0) + { + Console.Write($"\rProcessed {processedCount}/{validTypes.Count} types..."); + } + + try + { + var apiClass = new ApiClass(type, options); + var members = apiClass.ToDictionary(); + + if (members.Any()) + { + // Add class summary + var classKey = $"{type.FullName}.__summary__"; + components[classKey] = new ComponentEntry + { + Summary = apiClass.Summary, + Signature = type.Name + }; + + // Add members + foreach (var member in members) + { + var memberKey = $"{type.FullName}.{member.Key}"; + components[memberKey] = new ComponentEntry + { + Summary = member.Value, + Signature = member.Key + }; + } + } + } + catch (Exception ex) + { + Console.WriteLine(); + Console.WriteLine($"[WARNING] Error processing type {type.FullName}: {ex.Message}"); + } + } + + Console.WriteLine(); + Console.WriteLine($"✓ Processed {validTypes.Count} types, generated {components.Count} documentation entries."); + + return new SummaryDocumentationData + { + Metadata = new SummaryMetadata + { + AssemblyVersion = version, + DateUtc = date, + Mode = Mode.ToString() + }, + Components = components + }; + } + + /// + /// Gets assembly version and current date. + /// + private (string Version, string Date) GetAssemblyInfo() + { + var version = "Unknown"; + + var versionAttribute = Assembly.GetCustomAttribute(); + if (versionAttribute != null) + { + var versionString = versionAttribute.InformationalVersion; + var plusIndex = versionString.IndexOf('+'); + + if (plusIndex >= 0 && plusIndex + 9 < versionString.Length) + { + version = versionString[..(plusIndex + 9)]; + } + else + { + version = versionString; + } + } + + var date = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); + + return (version, date); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/ComponentInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/ComponentInfo.cs new file mode 100644 index 0000000000..a27c17d8ff --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/ComponentInfo.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Represents a Fluent UI component with its full documentation. +/// +public class ComponentInfo +{ + /// + /// Gets or sets the name of the component. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the full type name of the component. + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Gets or sets a brief description of the component. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Summary { get; set; } + + /// + /// Gets or sets the category of the component. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Category { get; set; } + + /// + /// Gets or sets whether the component is a generic type. + /// Only serialized when true to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsGeneric { get; set; } + + /// + /// Gets or sets the base class name. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BaseClass { get; set; } + + /// + /// Gets or sets the list of properties. + /// Only serialized when not empty to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List? Properties { get; set; } + + /// + /// Gets or sets the list of events. + /// Only serialized when not empty to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List? Events { get; set; } + + /// + /// Gets or sets the list of methods. + /// Only serialized when not empty to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List? Methods { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/DocumentationMetadata.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/DocumentationMetadata.cs new file mode 100644 index 0000000000..11d6735003 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/DocumentationMetadata.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Metadata about the generated documentation. +/// +public class DocumentationMetadata +{ + /// + /// Gets or sets the assembly version. + /// + public string AssemblyVersion { get; set; } = string.Empty; + + /// + /// Gets or sets the generation date in UTC. + /// + public string GeneratedDateUtc { get; set; } = string.Empty; + + /// + /// Gets or sets the total component count. + /// + public int ComponentCount { get; set; } + + /// + /// Gets or sets the total enum count. + /// + public int EnumCount { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/DocumentationRoot.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/DocumentationRoot.cs new file mode 100644 index 0000000000..9930c23170 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/DocumentationRoot.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Root model for the MCP documentation JSON file. +/// Contains all components and enums documentation. +/// +public class DocumentationRoot +{ + /// + /// Gets or sets metadata about the generated documentation. + /// + public DocumentationMetadata Metadata { get; set; } = new(); + + /// + /// Gets or sets the list of all components. + /// + public List Components { get; set; } = []; + + /// + /// Gets or sets the list of all enums. + /// + public List Enums { get; set; } = []; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EnumInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EnumInfo.cs new file mode 100644 index 0000000000..a44868ca7b --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EnumInfo.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Represents an enum type. +/// +public class EnumInfo +{ + /// + /// Gets or sets the name of the enum. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the full type name of the enum. + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the enum. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Description { get; set; } + + /// + /// Gets or sets the enum values. + /// + public List Values { get; set; } = []; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EnumValueInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EnumValueInfo.cs new file mode 100644 index 0000000000..d6b98e7166 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EnumValueInfo.cs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Represents an enum value. +/// +public class EnumValueInfo +{ + /// + /// Gets or sets the name of the enum value. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the numeric value. + /// + public int Value { get; set; } + + /// + /// Gets or sets the description of the enum value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Description { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EventInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EventInfo.cs new file mode 100644 index 0000000000..d6861bcf88 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/EventInfo.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Represents an event of a component. +/// +public class EventInfo +{ + /// + /// Gets or sets the name of the event. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the type of the event callback. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the event. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Description { get; set; } + + /// + /// Gets or sets whether this event is inherited. + /// Only serialized when true to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsInherited { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/MethodInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/MethodInfo.cs new file mode 100644 index 0000000000..f7b91c948d --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/MethodInfo.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Represents a method of a component. +/// +public class MethodInfo +{ + /// + /// Gets or sets the name of the method. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the return type of the method. + /// + public string ReturnType { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the method. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Description { get; set; } + + /// + /// Gets or sets the parameters of the method. + /// Only serialized when not empty to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string[]? Parameters { get; set; } + + /// + /// Gets or sets whether this method is inherited. + /// Only serialized when true to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsInherited { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/PropertyInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/PropertyInfo.cs new file mode 100644 index 0000000000..93ac884006 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/AllMode/PropertyInfo.cs @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace FluentUI.Demo.DocApiGen.Models.AllMode; + +/// +/// Represents a property of a component. +/// +public class PropertyInfo +{ + /// + /// Gets or sets the name of the property. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the type of the property. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the property. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Description { get; set; } + + /// + /// Gets or sets whether this property is a [Parameter]. + /// + public bool IsParameter { get; set; } + + /// + /// Gets or sets whether this property is inherited. + /// Only serialized when true to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsInherited { get; set; } + + /// + /// Gets or sets the default value of the property. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? DefaultValue { get; set; } + + /// + /// Gets or sets the enum values if this property is an enum type. + /// Only serialized when not empty to reduce JSON size. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string[]? EnumValues { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs index 25195753a2..e69de29bb2 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------ -// This file is licensed to you under the MIT License. -// ------------------------------------------------------------------------ - -using System.Reflection; - -namespace FluentUI.Demo.DocApiGen.Models; - -/// -/// Represents the options for the class generation. -/// -public class ApiClassOptions -{ - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ApiClassOptions(Assembly assembly, LoxSmoke.DocXml.DocXmlReader docReader) - { - Assembly = assembly; - DocXmlReader = docReader; - } - - /// - /// Gets the assembly to generate the documentation. - /// - public Assembly Assembly { get; } - - /// - /// Gets the summary reader. - /// - public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } - - /// - /// Gets or sets whether to include all properties (false) or only those with [Parameter] attribute (true). - /// - public bool PropertyParameterOnly { get; set; } = true; -} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs similarity index 80% rename from examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs rename to examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs index 54240c530d..d64087d26b 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; -namespace FluentUI.Demo.DocApiGen.Models; +namespace FluentUI.Demo.DocApiGen.Models.SummaryMode; /// /// Represents a class with properties, methods, and events. @@ -64,7 +64,22 @@ public Type[] InstanceTypes /// /// Gets the list of properties for the specified component. /// - public IEnumerable Properties => GetMembers(MemberTypes.Property).Where(i => _options.PropertyParameterOnly == false ? true : i.IsParameter); + public IEnumerable Properties + { + get + { + var properties = GetMembers(MemberTypes.Property); + + // For enums, include all values regardless of PropertyParameterOnly setting + if (_component.IsEnum) + { + return properties; + } + + // For classes, apply PropertyParameterOnly filter + return properties.Where(i => _options.PropertyParameterOnly == false || i.IsParameter); + } + } /// /// Gets the list of Events for the specified component. @@ -103,6 +118,7 @@ private IEnumerable GetMembers(MemberTypes type) List? members = []; object? obj = null; var created = false; + var canCreateInstance = CanCreateInstance(_component); var ctorArguments = HasCtorWithArguments(_component, ["LibraryConfiguration"]) ? new object?[] { null } @@ -111,16 +127,21 @@ private IEnumerable GetMembers(MemberTypes type) // Create an instance of the component to get the default values object? GetObjectValue(string propertyName) { - try + // Skip instance creation if we know it will fail + if (!canCreateInstance || _component.IsAbstract || _component.IsInterface) { + return null; + } + try + { if (!created) { if (_component.IsGenericType) { if (InstanceTypes is null) { - throw new InvalidCastException("InstanceTypes must be specified when Component is a generic type"); + return null; } // Supply the type to create the generic instance with (needs to be an array) @@ -135,10 +156,11 @@ private IEnumerable GetMembers(MemberTypes type) } return obj?.GetType().GetProperty(propertyName)?.GetValue(obj); - } catch (Exception) { + // Mark as unable to create to avoid future attempts + canCreateInstance = false; return null; } } @@ -188,9 +210,9 @@ private IEnumerable GetMembers(MemberTypes type) // Parameters/properties if (!isEvent) { - // Icon? icon = null; + // Only try to get default value if we can create an instance and the property type is simple var defaultValue = ""; - if (propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof(string)) + if (canCreateInstance && (propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof(string))) { defaultValue = GetObjectValue(propertyInfo.Name)?.ToString(); } @@ -250,10 +272,10 @@ private IEnumerable GetMembers(MemberTypes type) } } } - catch (Exception) + catch (Exception ex) { - Console.WriteLine($"[ApiDocumentation] ERROR: Cannot found {_component.FullName} -> {memberInfo.Name}"); - throw; + Console.WriteLine($"[ApiDocumentation] ERROR: Cannot process {_component.FullName} -> {memberInfo.Name}: {ex.Message}"); + // Don't rethrow, continue with next member } } @@ -263,6 +285,45 @@ private IEnumerable GetMembers(MemberTypes type) return _allMembers.Where(i => i.MemberType == type); } + /// + /// Checks if a type can be instantiated. + /// + private static bool CanCreateInstance(Type type) + { + if (type.IsAbstract || type.IsInterface || type.IsGenericTypeDefinition) + { + return false; + } + + // Check if type has a parameterless constructor or a constructor with nullable LibraryConfiguration + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + if (constructors.Length == 0) + { + return false; + } + + foreach (var ctor in constructors) + { + var parameters = ctor.GetParameters(); + + // Parameterless constructor + if (parameters.Length == 0) + { + return true; + } + + // Constructor with single nullable LibraryConfiguration parameter + if (parameters.Length == 1 && + parameters[0].ParameterType.Name == "LibraryConfiguration" && + parameters[0].IsOptional == false) + { + return true; + } + } + + return false; + } + /// private string GetSummary(Type component, MemberInfo? member) { diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs new file mode 100644 index 0000000000..287a7c7d41 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using FluentUI.Demo.DocApiGen.Abstractions; + +namespace FluentUI.Demo.DocApiGen.Models.SummaryMode; + +/// +/// Represents the options for the class generation. +/// +public class ApiClassOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ApiClassOptions(Assembly assembly, LoxSmoke.DocXml.DocXmlReader docReader) + { + Assembly = assembly; + DocXmlReader = docReader; + } + + /// + /// Gets the assembly to generate the documentation. + /// + public Assembly Assembly { get; } + + /// + /// Gets the summary reader. + /// + public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } + + /// + /// Gets or sets whether to include all properties (false) or only those with [Parameter] attribute (true). + /// + public bool PropertyParameterOnly { get; set; } = true; + + /// + /// Gets or sets the generation mode (Summary or All). + /// Summary mode includes only [Parameter] properties (PropertyParameterOnly = true). + /// All mode includes all properties, methods, and events (PropertyParameterOnly = false). + /// + public GenerationMode Mode + { + get => PropertyParameterOnly ? GenerationMode.Summary : GenerationMode.All; + set => PropertyParameterOnly = value == GenerationMode.Summary; + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiMember.cs similarity index 97% rename from examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs rename to examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiMember.cs index cb10ffd36f..64ed355512 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiMember.cs @@ -5,7 +5,7 @@ using System.Reflection; using FluentUI.Demo.DocApiGen.Extensions; -namespace FluentUI.Demo.DocApiGen.Models; +namespace FluentUI.Demo.DocApiGen.Models.SummaryMode; /// /// Represents a member of a class (Property, Method, Event). diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/SummaryDocumentationData.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/SummaryDocumentationData.cs new file mode 100644 index 0000000000..8c22933606 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/SummaryDocumentationData.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.SummaryMode; + +/// +/// Root object for Summary mode documentation. +/// +public class SummaryDocumentationData +{ + /// + /// Gets or sets the metadata. + /// + public required SummaryMetadata Metadata { get; set; } + + /// + /// Gets or sets the components dictionary. + /// Key: Full member name (Type.Member) + /// Value: Summary and Signature + /// + public required Dictionary Components { get; set; } +} + +/// +/// Represents a component entry in Summary mode. +/// +public class ComponentEntry +{ + /// + /// Gets or sets the summary text. + /// + public string Summary { get; set; } = string.Empty; + + /// + /// Gets or sets the signature. + /// + public string Signature { get; set; } = string.Empty; +} + +/// +/// Metadata for Summary mode generated documentation. +/// +public class SummaryMetadata +{ + /// + /// Gets or sets the assembly version. + /// + public string AssemblyVersion { get; set; } = string.Empty; + + /// + /// Gets or sets the generation date (UTC). + /// + public string DateUtc { get; set; } = string.Empty; + + /// + /// Gets or sets the generation mode. + /// + public string Mode { get; set; } = string.Empty; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs index 830aa9967b..57f6466f97 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs @@ -2,24 +2,31 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Formatters; +using FluentUI.Demo.DocApiGen.Generators; using Microsoft.Extensions.Configuration; using System.Reflection; namespace FluentUI.Demo.DocApiGen; -/// +/// +/// Main entry point for the DocApiGen tool. +/// public class Program { - private static readonly System.Diagnostics.Stopwatch _watcher = new (); + private static readonly System.Diagnostics.Stopwatch _watcher = new(); - /// + /// + /// Main method. + /// public static void Main(string[] args) { _watcher.Start(); Console.WriteLine($"-------------------------------------------------------------------"); Console.WriteLine($" DocApiGen v{Assembly.GetEntryAssembly()?.GetName().Version} "); - Console.WriteLine($" A simple command line tool to generate the documentation classes. "); + Console.WriteLine($" A tool to generate API documentation from assemblies. "); Console.WriteLine($"-------------------------------------------------------------------"); Console.WriteLine(); @@ -29,41 +36,125 @@ public static void Main(string[] args) var dllFile = config["dll"]; var outputFile = config["output"]; var format = config["format"] ?? "json"; + var modeArg = config["mode"] ?? "summary"; // Help if (string.IsNullOrEmpty(xmlFile) || string.IsNullOrEmpty(dllFile)) { - Console.WriteLine("Usage: DocApiGen --xml " + - " --dll " + - " --output " + - " --format "); + ShowHelp(); return; } - // Assembly and documentation file - var assembly = Assembly.LoadFrom(dllFile); - var docXml = new FileInfo(xmlFile); - var apiGenerator = new ApiClassGenerator(assembly, docXml); - - Console.WriteLine("Generating documentation..."); - if (!string.IsNullOrEmpty(outputFile)) - { - apiGenerator.SaveToFile(outputFile, format); - Console.WriteLine($"Documentation saved to {outputFile}"); - } - else + try { + // Parse generation mode + var mode = ParseMode(modeArg); + + // Validate format compatibility + ValidateFormatCompatibility(mode, format); + + // Load assembly and XML documentation + var assembly = Assembly.LoadFrom(dllFile); + var docXml = new FileInfo(xmlFile); + + Console.WriteLine("Generating documentation..."); + Console.WriteLine($" Assembly: {assembly.GetName().Name}"); + Console.WriteLine($" Mode: {mode}"); + Console.WriteLine($" Format: {format}"); Console.WriteLine(); - if (format == "json") + + // Create generator and formatter + var generator = DocumentationGeneratorFactory.Create(mode, assembly, docXml); + var formatter = OutputFormatterFactory.Create(format); + + // Generate and output + if (!string.IsNullOrEmpty(outputFile)) { - Console.WriteLine(apiGenerator.GenerateJson()); + generator.SaveToFile(outputFile, formatter); + Console.WriteLine($"✓ Documentation saved to: {outputFile}"); } else { - Console.WriteLine(apiGenerator.GenerateCSharp()); + var output = generator.Generate(formatter); + Console.WriteLine(output); + } + + _watcher.Stop(); + Console.WriteLine(); + Console.WriteLine($"✓ Completed in {_watcher.ElapsedMilliseconds} ms"); + } + catch (Exception ex) + { + if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + Console.ForegroundColor = ConsoleColor.Red; } + + Console.WriteLine($"✗ Error: {ex.Message}"); + + if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + Console.ResetColor(); + } + + Environment.Exit(1); } + } - Console.WriteLine($"Completed in {_watcher.ElapsedMilliseconds} ms"); + private static void ShowHelp() + { + Console.WriteLine("Usage:"); + Console.WriteLine(" DocApiGen --xml --dll [options]"); + Console.WriteLine(); + Console.WriteLine("Required Arguments:"); + Console.WriteLine(" --xml Path to the XML documentation file"); + Console.WriteLine(" --dll Path to the assembly DLL file"); + Console.WriteLine(); + Console.WriteLine("Optional Arguments:"); + Console.WriteLine(" --output Path to the output file (default: stdout)"); + Console.WriteLine(" --format Output format (default: json)"); + Console.WriteLine(" --mode Generation mode (default: summary)"); + Console.WriteLine(); + Console.WriteLine("Formats:"); + Console.WriteLine(" json - Generate JSON documentation"); + Console.WriteLine(" csharp - Generate C# code with documentation dictionary"); + Console.WriteLine(); + Console.WriteLine("Modes:"); + Console.WriteLine(" summary - Generate documentation with only [Parameter] properties"); + Console.WriteLine(" Supports: json, csharp"); + Console.WriteLine(" all - Generate complete documentation (properties, methods, events)"); + Console.WriteLine(" Supports: json only"); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" # Generate Summary mode JSON"); + Console.WriteLine(" DocApiGen --xml MyApp.xml --dll MyApp.dll --output api-summary.json"); + Console.WriteLine(); + Console.WriteLine(" # Generate Summary mode C#"); + Console.WriteLine(" DocApiGen --xml MyApp.xml --dll MyApp.dll --output CodeComments.cs --format csharp"); + Console.WriteLine(); + Console.WriteLine(" # Generate All mode JSON"); + Console.WriteLine(" DocApiGen --xml MyApp.xml --dll MyApp.dll --output api-all.json --mode all"); + } + + private static GenerationMode ParseMode(string modeArg) + { + return modeArg.ToLowerInvariant() switch + { + "summary" => GenerationMode.Summary, + "all" => GenerationMode.All, + _ => throw new ArgumentException($"Invalid mode '{modeArg}'. Valid modes are: summary, all") + }; + } + + private static void ValidateFormatCompatibility(GenerationMode mode, string format) + { + var formatLower = format.ToLowerInvariant(); + + // All mode only supports JSON + if (mode == GenerationMode.All && formatLower != "json") + { + throw new NotSupportedException( + $"Mode 'all' only supports JSON format. Requested format: {format}"); + } } } diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json b/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json index 1619966f4a..717f3739e2 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json @@ -3,6 +3,10 @@ "FluentUI.Demo.DocApiGen": { "commandName": "Project", "commandLineArgs": "--xml \"$(MSBuildProjectDirectory)/Microsoft.FluentUI.AspNetCore.Components.xml\" --dll \"$(SolutionDir)src/Core/bin/$(Configuration)/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll\" --output \"$(SolutionDir)examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments.json\" --format json" + }, + "FluentUI.Demo.DocApiGen all": { + "commandName": "Project", + "commandLineArgs": "--xml \"$(MSBuildProjectDirectory)/Microsoft.FluentUI.AspNetCore.Components.xml\" --dll \"$(SolutionDir)src/Core/bin/$(Configuration)/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll\" --output \"$(SolutionDir)examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments-all.json\" --format json --mode all" } } } diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/ReadMe.md b/examples/Tools/FluentUI.Demo.DocApiGen/ReadMe.md index 51c86150b8..fd1477bebb 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/ReadMe.md +++ b/examples/Tools/FluentUI.Demo.DocApiGen/ReadMe.md @@ -5,16 +5,55 @@ you can generate it using this project `FluentUI.Demo.DocApiGen`. Simply run the project `FluentUI.Demo.DocApiGen` to generate the file. -From Visual Studio +## From Visual Studio 1. Right-click on the project `FluentUI.Demo.DocApiGen`. 1. Select the menu `Debug` > `Start without debugging`. 1. Re-run the project `FluentUI.Demo` to see the changes. -From command line +## From command line -1. Open a command line in the folder `FluentUI.Demo.DocApiGen`. -1. Run the command - ``` - dotnet run --xml "./Microsoft.FluentUI.AspNetCore.Components.xml" --dll "../../../src/Core/bin/Debug/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll" --output "../../../examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments.json" --format json - ``` +The tool uses the **compact format by default** for Summary mode, which is optimized for documentation display. + +### Generate Summary mode (default, compact format) + +```bash +dotnet run --xml "./Microsoft.FluentUI.AspNetCore.Components.xml" --dll "../../../src/Core/bin/Debug/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll" --output "../../../examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments.json" --format json +``` + +This will generate a JSON file with the compact structure optimized for Summary mode: +```json +{ + "__Generated__": { + "AssemblyVersion": "5.0.0-alpha.1+07c679a2", + "DateUtc": "2025-12-13 21:33" + }, + "FluentButton": { + "__summary__": "The FluentButton component...", + "Appearance": "Gets or sets the visual appearance...", + ... + } +} +``` + +### Generate All mode (complete documentation) + +```bash +dotnet run --xml "./Microsoft.FluentUI.AspNetCore.Components.xml" --dll "../../../src/Core/bin/Debug/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll" --output "../../../examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments-all.json" --format json --mode all +``` + +## Documentation Formats + +The tool supports different JSON formats depending on the generation mode: + +1. **Summary Mode - Compact Format** (default): Optimized for documentation display + - Uses `__Generated__` for metadata + - Simple component names as keys + - Direct member properties + - Best for quick lookups and UI display + +2. **All Mode - Structured Format**: Complete documentation with all members + - Uses `metadata` and `components` sections + - CamelCase property names + - Full type names with namespaces + - Includes properties, methods, events, and enums