From c736a3db39c88c57f72cc78d291b4b039dde330d Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 2 Apr 2024 12:49:41 -0500 Subject: [PATCH 1/6] fix: changes in YamlDotNet to allow AOT --- .build.Nuke/Build.Publish.cs | 1 - .editorconfig | 2 + .nuke/build.schema.json | 328 +++++++++++++------------- source/CheckLinkCommand.cs | 6 +- source/Helpers/SourceFileWatcher.cs | 2 +- source/Models/FrontMatter.cs | 23 +- source/Models/FrontMatterResources.cs | 2 + source/Models/IFrontMatter.cs | 6 +- source/Models/Page.cs | 15 +- source/Models/Resource.cs | 11 +- source/Models/SiteSettings.cs | 7 +- source/Parser/StaticContext.cs | 11 + source/Parser/YAMLParser.cs | 38 ++- source/Program.cs | 5 + source/SuCoS.csproj | 14 +- 15 files changed, 260 insertions(+), 211 deletions(-) create mode 100644 source/Parser/StaticContext.cs diff --git a/.build.Nuke/Build.Publish.cs b/.build.Nuke/Build.Publish.cs index 042775f..09d26d9 100644 --- a/.build.Nuke/Build.Publish.cs +++ b/.build.Nuke/Build.Publish.cs @@ -40,7 +40,6 @@ sealed partial class Build : NukeBuild .SetSelfContained(publishSelfContained) .SetPublishSingleFile(publishSingleFile) .SetPublishTrimmed(publishTrimmed) - .SetAuthors("Bruno Massa") .SetVersion(CurrentVersion) .SetAssemblyVersion(CurrentVersion) .SetInformationalVersion(CurrentVersion) diff --git a/.editorconfig b/.editorconfig index ad36a90..ca0a0b9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -68,3 +68,5 @@ dotnet_diagnostic.CS8019.severity = warning [**/obj/**/*.cs] dotnet_diagnostic.CS8019.severity = none # disable on debug genereated files +exclude = true +generated_code = true diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 87122c0..7def812 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,164 +1,164 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/build", - "title": "Build Schema", - "definitions": { - "build": { - "type": "object", - "properties": { - "configuration": { - "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)" - }, - "containerDefaultRID": { - "type": "string", - "description": "GitLab Project Full Address" - }, - "containerRegistryImage": { - "type": "string", - "description": "GitLab Project CI_REGISTRY_IMAGE" - }, - "Continue": { - "type": "boolean", - "description": "Indicates to continue a previously failed build attempt" - }, - "gitlabPrivateToken": { - "type": "string", - "description": "GitLab private token" - }, - "Help": { - "type": "boolean", - "description": "Shows the help text for this build assembly" - }, - "Host": { - "type": "string", - "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] - }, - "isScheduled": { - "type": "boolean", - "description": "If the pipeline was triggered by a schedule (or manually)" - }, - "NoLogo": { - "type": "boolean", - "description": "Disables displaying the NUKE logo" - }, - "packageName": { - "type": "string", - "description": "package-name (default: SuCoS)" - }, - "Partition": { - "type": "string", - "description": "Partition to use on CI" - }, - "Plan": { - "type": "boolean", - "description": "Shows the execution plan (HTML)" - }, - "Profile": { - "type": "array", - "description": "Defines the profiles to load", - "items": { - "type": "string" - } - }, - "publishDirectory": { - "type": "string", - "description": "publish-directory (default: ./publish/{runtimeIdentifier})" - }, - "publishSelfContained": { - "type": "boolean", - "description": "publish-self-contained (default: true)" - }, - "publishSingleFile": { - "type": "boolean", - "description": "publish-single-file (default: true)" - }, - "publishTrimmed": { - "type": "boolean", - "description": "publish-trimmed (default: false)" - }, - "Root": { - "type": "string", - "description": "Root directory during build execution" - }, - "runtimeIdentifier": { - "type": "string", - "description": "Runtime identifier for the build (e.g., win-x64, linux-x64, osx-x64) (default: linux-x64)" - }, - "Skip": { - "type": "array", - "description": "List of targets to be skipped. Empty list skips all dependencies", - "items": { - "type": "string", - "enum": [ - "CheckNewCommits", - "Clean", - "Compile", - "CreateContainer", - "CreatePackage", - "GitLabCreateRelease", - "GitLabCreateTag", - "Publish", - "Restore", - "ShowCurrentVersion", - "Test", - "TestReport" - ] - } - }, - "solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, - "Target": { - "type": "array", - "description": "List of targets to be invoked. Default is '{default_target}'", - "items": { - "type": "string", - "enum": [ - "CheckNewCommits", - "Clean", - "Compile", - "CreateContainer", - "CreatePackage", - "GitLabCreateRelease", - "GitLabCreateTag", - "Publish", - "Restore", - "ShowCurrentVersion", - "Test", - "TestReport" - ] - } - }, - "Verbosity": { - "type": "string", - "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] - } - } - } - } -} +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/build", + "title": "Build Schema", + "definitions": { + "build": { + "type": "object", + "properties": { + "configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)" + }, + "containerDefaultRID": { + "type": "string", + "description": "GitLab Project Full Address" + }, + "containerRegistryImage": { + "type": "string", + "description": "GitLab Project CI_REGISTRY_IMAGE" + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "gitlabPrivateToken": { + "type": "string", + "description": "GitLab private token" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "isScheduled": { + "type": "boolean", + "description": "If the pipeline was triggered by a schedule (or manually)" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "packageName": { + "type": "string", + "description": "package-name (default: SuCoS)" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "publishDirectory": { + "type": "string", + "description": "publish-directory (default: ./publish/{runtimeIdentifier})" + }, + "publishSelfContained": { + "type": "boolean", + "description": "publish-self-contained (default: true)" + }, + "publishSingleFile": { + "type": "boolean", + "description": "publish-single-file (default: true)" + }, + "publishTrimmed": { + "type": "boolean", + "description": "publish-trimmed (default: false)" + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "runtimeIdentifier": { + "type": "string", + "description": "Runtime identifier for the build (e.g., win-x64, linux-x64, osx-x64) (default: linux-x64)" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "CheckNewCommits", + "Clean", + "Compile", + "CreateContainer", + "CreatePackage", + "GitLabCreateRelease", + "GitLabCreateTag", + "Publish", + "Restore", + "ShowCurrentVersion", + "Test", + "TestReport" + ] + } + }, + "solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "CheckNewCommits", + "Clean", + "Compile", + "CreateContainer", + "CreatePackage", + "GitLabCreateRelease", + "GitLabCreateTag", + "Publish", + "Restore", + "ShowCurrentVersion", + "Test", + "TestReport" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} diff --git a/source/CheckLinkCommand.cs b/source/CheckLinkCommand.cs index f978458..a98dc92 100644 --- a/source/CheckLinkCommand.cs +++ b/source/CheckLinkCommand.cs @@ -14,8 +14,8 @@ public sealed partial class CheckLinkCommand(CheckLinkOptions settings, ILogger { [GeneratedRegex(@"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)")] - private static partial Regex MyRegex(); - private static readonly Regex linkRegex = MyRegex(); + private static partial Regex URLRegex(); + private static readonly Regex urlRegex = URLRegex(); private const int retriesCount = 3; private readonly TimeSpan retryInterval = TimeSpan.FromSeconds(1); private HttpClient httpClient = null!; @@ -77,7 +77,7 @@ public sealed partial class CheckLinkCommand(CheckLinkOptions settings, ILogger { var fileNameSanitized = filePath[directoryPath.Length..].Trim('/', '\\'); var fileText = File.ReadAllText(filePath); - var matches = linkRegex.Matches(fileText); + var matches = urlRegex.Matches(fileText); if (matches.Count == 0) { LogInformation("{fileName}: no links found", fileNameSanitized); diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs index 4da2e2d..bcacae6 100644 --- a/source/Helpers/SourceFileWatcher.cs +++ b/source/Helpers/SourceFileWatcher.cs @@ -17,7 +17,7 @@ public sealed class SourceFileWatcher : IFileWatcher, IDisposable public void Start(string SourceAbsolutePath, Action OnSourceFileChanged) { ArgumentNullException.ThrowIfNull(OnSourceFileChanged); - + fileWatcher = new FileSystemWatcher { Path = SourceAbsolutePath, diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index f415158..233d25d 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -7,48 +7,49 @@ namespace SuCoS.Models; /// A scafold structure to help creating system-generated content, like /// tag, section or index pages /// +[YamlSerializable] public class FrontMatter : IFrontMatter { #region IFrontMatter /// - public string? Title { get; init; } = string.Empty; + public string? Title { get; set; } = string.Empty; /// public string? Type { get; set; } = "page"; /// - public string? URL { get; init; } + public string? URL { get; set; } /// - public bool? Draft { get; init; } + public bool? Draft { get; set; } /// - public Collection? Aliases { get; init; } + public List? Aliases { get; set; } /// public string? Section { get; set; } = string.Empty; /// - public DateTime? Date { get; init; } + public DateTime? Date { get; set; } /// - public DateTime? LastMod { get; init; } + public DateTime? LastMod { get; set; } /// - public DateTime? PublishDate { get; init; } + public DateTime? PublishDate { get; set; } /// - public DateTime? ExpiryDate { get; init; } + public DateTime? ExpiryDate { get; set; } /// - public int Weight { get; init; } + public int Weight { get; set; } /// - public Collection? Tags { get; init; } + public List? Tags { get; set; } /// - public Collection? ResourceDefinitions { get; set; } + public List? ResourceDefinitions { get; set; } /// [YamlIgnore] diff --git a/source/Models/FrontMatterResources.cs b/source/Models/FrontMatterResources.cs index 6e63b9e..093b013 100644 --- a/source/Models/FrontMatterResources.cs +++ b/source/Models/FrontMatterResources.cs @@ -1,10 +1,12 @@ using Microsoft.Extensions.FileSystemGlobbing; +using YamlDotNet.Serialization; namespace SuCoS.Models; /// /// Basic structure needed to generate user content and system content /// +[YamlSerializable] public class FrontMatterResources : IFrontMatterResources { /// diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs index 50d7361..27d6c2a 100644 --- a/source/Models/IFrontMatter.cs +++ b/source/Models/IFrontMatter.cs @@ -83,7 +83,7 @@ public interface IFrontMatter : IParams, IFile /// List URL, it will be parsed as liquid templates, so you can use page variables. /// /// - Collection? Aliases { get; } + List? Aliases { get; } /// /// Page weight. Used for sorting by default. @@ -93,12 +93,12 @@ public interface IFrontMatter : IParams, IFile /// /// A list of tags, if any. /// - Collection? Tags { get; } + List? Tags { get; } /// /// List of resource definitions. /// - Collection? ResourceDefinitions { get; } + List? ResourceDefinitions { get; set; } /// /// Raw content from the Markdown file, bellow the front matter. diff --git a/source/Models/Page.cs b/source/Models/Page.cs index ff2201d..503ef96 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -29,7 +29,7 @@ public class Page : IPage public bool? Draft => frontMatter.Draft; /// - public Collection? Aliases => frontMatter.Aliases; + public List? Aliases => frontMatter.Aliases; /// public string? Section => frontMatter.Section; @@ -50,10 +50,14 @@ public class Page : IPage public int Weight => frontMatter.Weight; /// - public Collection? Tags => frontMatter.Tags; + public List? Tags => frontMatter.Tags; /// - public Collection? ResourceDefinitions => frontMatter.ResourceDefinitions; + public List? ResourceDefinitions + { + get => frontMatter.ResourceDefinitions; + set => frontMatter.ResourceDefinitions = value; + } /// public string RawContent => frontMatter.RawContent; @@ -445,12 +449,13 @@ endif } filename = Path.GetFileNameWithoutExtension(filename) + extention; - var resource = new Resource(resourceFilename) + var resource = new Resource() { Title = title, FileName = filename, Permalink = Path.Combine(Permalink!, filename), - Params = resourceParams + Params = resourceParams, + SourceFullPath = resourceFilename }; Resources.Add(resource); } diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs index 5082bd7..05745cc 100644 --- a/source/Models/Resource.cs +++ b/source/Models/Resource.cs @@ -1,8 +1,11 @@ +using YamlDotNet.Serialization; + namespace SuCoS.Models; /// /// Page resources. All files that accompany a page. /// +[YamlSerializable] public class Resource : IResource { /// @@ -15,7 +18,7 @@ public class Resource : IResource public string SourceFullPath { get; set; } /// - public string? SourceRelativePath => throw new NotImplementedException(); + public string? SourceRelativePath => null; /// public string? Permalink { get; set; } @@ -26,9 +29,5 @@ public class Resource : IResource /// /// Default constructor. /// - /// - public Resource(string path) - { - SourceFullPath = path; - } + public Resource() { } } diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index e2efd0c..fc06d15 100644 --- a/source/Models/SiteSettings.cs +++ b/source/Models/SiteSettings.cs @@ -1,8 +1,11 @@ +using YamlDotNet.Serialization; + namespace SuCoS.Models; /// /// The main configuration of the program, extracted from the app.yaml file. /// +[YamlSerializable] public class SiteSettings : IParams { /// @@ -33,7 +36,7 @@ public class SiteSettings : IParams #region IParams /// - public Dictionary Params { get; set; } = []; + public Dictionary? Params { get; set; } = []; #endregion IParams -} \ No newline at end of file +} diff --git a/source/Parser/StaticContext.cs b/source/Parser/StaticContext.cs new file mode 100644 index 0000000..c0bcf0c --- /dev/null +++ b/source/Parser/StaticContext.cs @@ -0,0 +1,11 @@ +using YamlDotNet.Serialization; + +namespace SuCoS.Parser; + +/// +/// The rest of this partial class gets generated at build time +/// +[YamlStaticContext] +public partial class StaticAOTContext : StaticContext +{ +} diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 0f8e42d..21d1031 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -14,16 +14,30 @@ public class YAMLParser : IFrontMatterParser /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields /// - private readonly IDeserializer yamlDeserializerRigid = new DeserializerBuilder() - .IgnoreUnmatchedProperties() - .Build(); + private readonly IDeserializer yamlDeserializerRigid; /// /// YamlDotNet parser to loosely parse the YAML file. Used to include all non-matching fields /// into Params. /// - private readonly IDeserializer yamlDeserializer = new DeserializerBuilder() - .Build(); + private readonly IDeserializer yamlDeserializer; + + private readonly StaticAOTContext staticAOTContext; + + /// + /// ctor + /// + public YAMLParser() + { + staticAOTContext = new(); + + yamlDeserializerRigid = new StaticDeserializerBuilder(staticAOTContext) + .IgnoreUnmatchedProperties() + .Build(); + + yamlDeserializer = new StaticDeserializerBuilder(staticAOTContext) + .Build(); + } /// public IFrontMatter ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string? sourceContentPath = null) @@ -80,12 +94,12 @@ public class YAMLParser : IFrontMatterParser frontMatter.SourceFullPath = fileFullPath; frontMatter.Type ??= section; - var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml)); - if (yamlObject is not Dictionary yamlDictionary) - { - return frontMatter; - } - ParseParams(frontMatter, typeof(Page), yaml, yamlDictionary); + // var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml)); + // if (yamlObject is not Dictionary yamlDictionary) + // { + // return frontMatter; + // } + // ParseParams(frontMatter, typeof(Page), yaml, yamlDictionary); return frontMatter; } @@ -94,7 +108,7 @@ public class YAMLParser : IFrontMatterParser public SiteSettings ParseSiteSettings(string yaml) { var settings = yamlDeserializerRigid.Deserialize(yaml); - ParseParams(settings, typeof(SiteSettings), yaml); + // ParseParams(settings, typeof(SiteSettings), yaml); return settings; } diff --git a/source/Program.cs b/source/Program.cs index 70bc94a..3a603d5 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -4,6 +4,7 @@ using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; using System.Reflection; using CommandLine; +using System.Diagnostics.CodeAnalysis; namespace SuCoS; @@ -40,6 +41,10 @@ public class Program(ILogger logger) /// /// /// + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GenerateOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ServeOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CheckLinkOptions))] public async Task RunCommandLine(string[] args) { return await CommandLine.Parser.Default.ParseArguments(args) diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index ff63bde..fe2ccc1 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -7,6 +7,13 @@ enable true true + true + true + true + Bruno Massa + ../LICENSE + hhttps://sucos.brunomassa.com + static site generator;ssg;yaml/blog @@ -15,15 +22,16 @@ - - - + + + + -- GitLab From e1e32929c6f655a94b2e336de8fb89b7b3f725d1 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 2 Apr 2024 14:01:19 -0500 Subject: [PATCH 2/6] fix: replace AOT for PublishReadyToRun --- .build.Nuke/Build.Publish.cs | 4 ++++ .nuke/build.schema.json | 4 ++++ source/SuCoS.csproj | 1 - 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.build.Nuke/Build.Publish.cs b/.build.Nuke/Build.Publish.cs index 09d26d9..ecc39b5 100644 --- a/.build.Nuke/Build.Publish.cs +++ b/.build.Nuke/Build.Publish.cs @@ -27,6 +27,9 @@ sealed partial class Build : NukeBuild [Parameter("publish-trimmed (default: false)")] readonly bool publishTrimmed = false; + [Parameter("publish-ready-to-run (default: true)")] + readonly bool publishReadyToRun = true; + Target Publish => td => td .DependsOn(Restore) .Executes(() => @@ -40,6 +43,7 @@ sealed partial class Build : NukeBuild .SetSelfContained(publishSelfContained) .SetPublishSingleFile(publishSingleFile) .SetPublishTrimmed(publishTrimmed) + .SetPublishReadyToRun(publishReadyToRun) .SetVersion(CurrentVersion) .SetAssemblyVersion(CurrentVersion) .SetInformationalVersion(CurrentVersion) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 7def812..5990054 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -82,6 +82,10 @@ "type": "string", "description": "publish-directory (default: ./publish/{runtimeIdentifier})" }, + "publishReadyToRun": { + "type": "boolean", + "description": "publish-ready-to-run (default: true)" + }, "publishSelfContained": { "type": "boolean", "description": "publish-self-contained (default: true)" diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index fe2ccc1..32eb4dc 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -7,7 +7,6 @@ enable true true - true true true Bruno Massa -- GitLab From 8c854de8f527fa34b27c0b32a0d70f87441355cc Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 2 Apr 2024 15:23:56 -0500 Subject: [PATCH 3/6] fix: remove YamlSerializable in Resource --- source/Models/Resource.cs | 10 +--------- source/Models/SiteSettings.cs | 6 +++--- test/Parser/YAMLParserTests.cs | 10 +++++----- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs index 05745cc..0bae647 100644 --- a/source/Models/Resource.cs +++ b/source/Models/Resource.cs @@ -1,11 +1,8 @@ -using YamlDotNet.Serialization; - namespace SuCoS.Models; /// /// Page resources. All files that accompany a page. /// -[YamlSerializable] public class Resource : IResource { /// @@ -15,7 +12,7 @@ public class Resource : IResource public string? FileName { get; set; } /// - public string SourceFullPath { get; set; } + public required string SourceFullPath { get; set; } /// public string? SourceRelativePath => null; @@ -25,9 +22,4 @@ public class Resource : IResource /// public Dictionary Params { get; set; } = []; - - /// - /// Default constructor. - /// - public Resource() { } } diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index fc06d15..29ec651 100644 --- a/source/Models/SiteSettings.cs +++ b/source/Models/SiteSettings.cs @@ -16,12 +16,12 @@ public class SiteSettings : IParams /// /// Site description /// - public string? Description { get; set; } + public string? Description { get; set; } = string.Empty; /// /// Copyright information /// - public string? Copyright { get; set; } + public string? Copyright { get; set; } = string.Empty; /// /// The base URL that will be used to build public links. @@ -36,7 +36,7 @@ public class SiteSettings : IParams #region IParams /// - public Dictionary? Params { get; set; } = []; + public Dictionary Params { get; set; } = []; #endregion IParams } diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index 2782a37..7cce772 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -143,14 +143,14 @@ Title public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() { // Act - var site = parser.ParseSiteSettings(siteContentCONST); + var siteSettings = parser.ParseSiteSettings(siteContentCONST); // Asset - Assert.Equal("My Site", site.Title); - Assert.Equal("https://www.example.com/", site.BaseURL); - Assert.Equal("Tastiest C# Static Site Generator of the World", site.Description); - Assert.Equal("Copyright message", site.Copyright); + Assert.Equal("My Site", siteSettings.Title); + Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + Assert.Equal("Tastiest C# Static Site Generator of the World", siteSettings.Description); + Assert.Equal("Copyright message", siteSettings.Copyright); } [Fact] -- GitLab From a39e15a0d21c070098f9bc46567ba8a08e1bb162 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Thu, 4 Apr 2024 13:46:55 -0500 Subject: [PATCH 4/6] fix: return 0 (success) for help and version --- source/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Program.cs b/source/Program.cs index 02a810e..6113c5e 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -99,7 +99,7 @@ public class Program(ILogger logger) var command = new NewSiteCommand(options, logger); return Task.FromResult(command.Run()); }, - errs => Task.FromResult(1) + errs => Task.FromResult(0) ); } -- GitLab From e7ec4c86ff158fd9bbf41ca61d4d2f6cd2fb3848 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 9 Apr 2024 01:05:50 -0500 Subject: [PATCH 5/6] fix: Fixed the yaml parser tests --- source/Models/FrontMatter.cs | 1 - source/Parser/YAMLParser.cs | 148 ++++++++--- test/Parser/YAMLParserTests.cs | 453 ++++++++++++++++----------------- test/TestSetup.cs | 10 +- 4 files changed, 343 insertions(+), 269 deletions(-) diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index 233d25d..b4ac972 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using YamlDotNet.Serialization; namespace SuCoS.Models; diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 21d1031..43f7954 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -1,7 +1,8 @@ +using System.Text; using SuCoS.Helpers; using SuCoS.Models; -using System.Diagnostics.CodeAnalysis; -using System.Text; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; using YamlDotNet.Serialization; namespace SuCoS.Parser; @@ -16,13 +17,8 @@ public class YAMLParser : IFrontMatterParser /// private readonly IDeserializer yamlDeserializerRigid; - /// - /// YamlDotNet parser to loosely parse the YAML file. Used to include all non-matching fields - /// into Params. - /// - private readonly IDeserializer yamlDeserializer; - private readonly StaticAOTContext staticAOTContext; + private readonly ISerializer yamlSerializer; /// /// ctor @@ -32,15 +28,18 @@ public class YAMLParser : IFrontMatterParser staticAOTContext = new(); yamlDeserializerRigid = new StaticDeserializerBuilder(staticAOTContext) - .IgnoreUnmatchedProperties() - .Build(); + .WithTypeConverter(new ParamsConverter()) + .IgnoreUnmatchedProperties() + .Build(); - yamlDeserializer = new StaticDeserializerBuilder(staticAOTContext) - .Build(); + yamlSerializer = new StaticSerializerBuilder(staticAOTContext).Build(); } /// - public IFrontMatter ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string? sourceContentPath = null) + public IFrontMatter ParseFrontmatterAndMarkdownFromFile( + in string fileFullPath, + in string? sourceContentPath = null + ) { ArgumentNullException.ThrowIfNull(fileFullPath); @@ -49,18 +48,29 @@ public class YAMLParser : IFrontMatterParser try { fileContent = File.ReadAllText(fileFullPath); - fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, fileFullPath); + fileRelativePath = Path.GetRelativePath( + sourceContentPath ?? string.Empty, + fileFullPath + ); } catch (Exception ex) { throw new FileNotFoundException(fileFullPath, ex); } - return ParseFrontmatterAndMarkdown(fileFullPath, fileRelativePath, fileContent); + return ParseFrontmatterAndMarkdown( + fileFullPath, + fileRelativePath, + fileContent + ); } /// - public IFrontMatter ParseFrontmatterAndMarkdown(in string fileFullPath, in string fileRelativePath, in string fileContent) + public IFrontMatter ParseFrontmatterAndMarkdown( + in string fileFullPath, + in string fileRelativePath, + in string fileContent + ) { ArgumentNullException.ThrowIfNull(fileRelativePath); @@ -84,9 +94,17 @@ public class YAMLParser : IFrontMatterParser return page; } - private FrontMatter ParseYAML(in string fileFullPath, in string fileRelativePath, string yaml, in string rawContent) + private FrontMatter ParseYAML( + in string fileFullPath, + in string fileRelativePath, + string yaml, + in string rawContent + ) { - var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter"); + var frontMatter = + yamlDeserializerRigid.Deserialize( + new StringReader(yaml) + ) ?? throw new FormatException("Error parsing front matter"); var section = SiteHelper.GetSection(fileRelativePath); frontMatter.RawContent = rawContent; frontMatter.Section = section; @@ -108,45 +126,99 @@ public class YAMLParser : IFrontMatterParser public SiteSettings ParseSiteSettings(string yaml) { var settings = yamlDeserializerRigid.Deserialize(yaml); - // ParseParams(settings, typeof(SiteSettings), yaml); return settings; } +} + +/// +/// A custom YAML type converter for dictionaries with string keys and object values. +/// +public class ParamsConverter : IYamlTypeConverter +{ + /// + /// Checks if the converter can handle the specified type. + /// + /// The type to check. + /// True if the type is a dictionary with string keys and object values, false otherwise. + public bool Accepts(Type type) + { + return type == typeof(Dictionary); + } /// - /// Parse all YAML files for non-matching fields. + /// Reads a YAML stream and deserializes it into a dictionary. /// - /// Site or Frontmatter object, that implements IParams - /// The type (Site or Frontmatter) - /// YAML content - /// yamlObject already parsed if available - public void ParseParams(IParams settings, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, string yaml, object? yamlObject = null) + /// The YAML parser. + /// The type of the object to deserialize. + /// A dictionary deserialized from the YAML stream. + public object ReadYaml(IParser parser, Type type) { - ArgumentNullException.ThrowIfNull(settings); - ArgumentNullException.ThrowIfNull(type); + var dictionary = new Dictionary(); - yamlObject ??= yamlDeserializer.Deserialize(new StringReader(yaml)); - if (yamlObject is not Dictionary yamlDictionary) + if (!parser.TryConsume(out _)) { - return; + // throw new YamlException("Expected a mapping start."); } - foreach (var key in yamlDictionary.Keys.Cast()) + while (!parser.TryConsume(out _)) { - // If the property is not a standard Frontmatter property - if (type.GetProperty(key) != null) + if (!parser.TryConsume(out var key)) { - continue; + throw new YamlException("Expected a key."); } - // Recursively create a dictionary structure for the value - if (yamlDictionary[key] is Dictionary valueDictionary) + if (parser.TryConsume(out _)) { - settings.Params[key] = valueDictionary; + var list = new List(); + while (!parser.TryConsume(out _)) + { + if (parser?.Current is MappingStart) + { + list.Add(ReadYaml(parser, type)); + } + else if (parser?.Current is Scalar) + { + if (parser.TryConsume(out var scalar)) + { + list.Add(scalar.Value); + } + } + else + { + throw new YamlException( + "Expected a value, a nested mapping, or a sequence end." + ); + } + } + + dictionary[key.Value] = list; + } + else if (parser.TryConsume(out _)) + { + var nestedDictionary = + (Dictionary)ReadYaml(parser, type); + dictionary[key.Value] = nestedDictionary; } else { - settings.Params[key] = yamlDictionary[key]; + if (parser.TryConsume(out var value)) + { + dictionary[key.Value] = value.Value; + } } } + + return dictionary; + } + + /// + /// Writes an object to a YAML stream. + /// + /// The YAML emitter. + /// The object to serialize. + /// The type of the object to serialize. + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); } } diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index 7cce772..8a4d577 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -1,16 +1,15 @@ using SuCoS.Helpers; using SuCoS.Models; -using SuCoS.Parser; using System.Globalization; using Xunit; -namespace Tests.Parser; +namespace Tests.YAMLParser; public class YAMLParserTests : TestSetup { - private readonly YAMLParser parser; + private readonly SuCoS.Parser.YAMLParser parser; - private const string pageFrontmaterCONST = @"Title: Test Title + private const string pageFrontmaterCONST = @"Title: Test Title Type: post Date: 2023-07-01 LastMod: 2023-06-01 @@ -27,13 +26,19 @@ NestedData: - Test - Real Data customParam: Custom Value +Params: + ParamsCustomParam: Custom Value + ParamsNestedData: + Level2: + - Test + - Real Data "; - private const string pageMarkdownCONST = @" + private const string pageMarkdownCONST = @" # Real Data Test This is a test using real data. Real Data Test "; - private const string siteContentCONST = @" + private const string siteContentCONST = @" Title: My Site BaseURL: https://www.example.com/ Description: Tastiest C# Static Site Generator of the World @@ -43,252 +48,244 @@ NestedData: Level2: - Test - Real Data +Params: + ParamsCustomParam: Custom Value + ParamsNestedData: + Level2: + - Test + - Real Data "; - private const string fileFullPathCONST = "test.md"; - private const string fileRelativePathCONST = "test.md"; - private readonly string pageContent; - - public YAMLParserTests() - { - parser = new YAMLParser(); - pageContent = @$"--- + private const string fileFullPathCONST = "test.md"; + private const string fileRelativePathCONST = "test.md"; + private readonly string pageContent; + + public YAMLParserTests() + { + parser = new SuCoS.Parser.YAMLParser(); + pageContent = @$"--- {pageFrontmaterCONST} --- {pageMarkdownCONST}"; - } + } - [Fact] - public void GetSection_ShouldReturnFirstFolderName() - { - var filePath = Path.Combine("folder1", "folder2", "file.md"); + [Fact] + public void GetSection_ShouldReturnFirstFolderName() + { + // Arrange + var filePath = Path.Combine("folder1", "folder2", "file.md"); - // Act - var section = SiteHelper.GetSection(filePath); + // Act + var section = SiteHelper.GetSection(filePath); - // Asset - Assert.Equal("folder1", section); - } + // Assert + Assert.Equal("folder1", section); + } - [Theory] - [InlineData(@"--- + [Theory] + [InlineData(@"--- Title: Test Title --- ", "Test Title")] - [InlineData(@"--- + [InlineData(@"--- Date: 2023-04-01 --- ", "")] - public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) - { - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); + public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) + { + // Arrange + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); - // Asset - Assert.Equal(expectedTitle, frontMatter.Title); - } + // Assert + Assert.Equal(expectedTitle, frontMatter.Title); + } - [Theory] - [InlineData(@"--- + [Theory] + [InlineData(@"--- Date: 2023-01-01 --- ", "2023-01-01")] - [InlineData(@"--- + [InlineData(@"--- Date: 2023/01/01 --- ", "2023-01-01")] - public void ParseFrontmatter_ShouldParseDateCorrectly(string fileContent, string expectedDateString) - { - var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); - - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); - - // Asset - Assert.Equal(expectedDate, frontMatter.Date); - } - - [Fact] - public void ParseFrontmatter_ShouldParseOtherFieldsCorrectly() - { - var expectedDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); - var expectedLastMod = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); - var expectedPublishDate = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); - var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); - - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, pageContent); - - // Asset - Assert.Equal("Test Title", frontMatter.Title); - Assert.Equal("post", frontMatter.Type); - Assert.Equal(expectedDate, frontMatter.Date); - Assert.Equal(expectedLastMod, frontMatter.LastMod); - Assert.Equal(expectedPublishDate, frontMatter.PublishDate); - Assert.Equal(expectedExpiryDate, frontMatter.ExpiryDate); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowException_WhenInvalidYAMLSyntax() - { - const string fileContent = @"--- + public void ParseFrontmatter_ShouldParseDateCorrectly(string fileContent, string expectedDateString) + { + // Arrange + var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); + + // Act + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); + + // Assert + Assert.Equal(expectedDate, frontMatter.Date); + } + + [Fact] + public void ParseFrontmatter_ShouldParseOtherFieldsCorrectly() + { + // Arrange + var expectedDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + var expectedLastMod = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); + var expectedPublishDate = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); + var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); + + // Act + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, pageContent); + + // Assert + Assert.Equal("Test Title", frontMatter.Title); + Assert.Equal("post", frontMatter.Type); + Assert.Equal(expectedDate, frontMatter.Date); + Assert.Equal(expectedLastMod, frontMatter.LastMod); + Assert.Equal(expectedPublishDate, frontMatter.PublishDate); + Assert.Equal(expectedExpiryDate, frontMatter.ExpiryDate); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowException_WhenInvalidYAMLSyntax() + { + // Arrange + const string fileContent = @"--- Title --- "; - // Asset - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent)); - } - - [Fact] - public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() - { - // Act - var siteSettings = parser.ParseSiteSettings(siteContentCONST); - - - // Asset - Assert.Equal("My Site", siteSettings.Title); - Assert.Equal("https://www.example.com/", siteSettings.BaseURL); - Assert.Equal("Tastiest C# Static Site Generator of the World", siteSettings.Description); - Assert.Equal("Copyright message", siteSettings.Copyright); - } - - [Fact] - public void ParseParams_ShouldFillParamsWithNonMatchingFields() - { - var page = new Page(new FrontMatter - { - Title = "Test Title", - SourceRelativePath = "/test.md" - }, site); - - // Act - parser.ParseParams(page, typeof(Page), pageFrontmaterCONST); - - // Asset - Assert.True(page.Params.ContainsKey("customParam")); - Assert.Equal("Custom Value", page.Params["customParam"]); - } - - [Fact] - public void ParseFrontmatter_ShouldParseContentInSiteFolder() - { - var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); - var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); - Page page = new(frontMatter, site); - - // Act - site.PostProcessPage(page); - - // Asset - Assert.Equal(date, frontMatter.Date); - } - - [Fact] - public void ParseFrontmatter_ShouldCreateTags() - { - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); - Page page = new(frontMatter, site); - - // Act - site.PostProcessPage(page); - - // Asset - Assert.Equal(2, page.TagsReference.Count); - } - - [Fact] - public void ParseFrontmatter_ShouldParseCategoriesCorrectly() - { - var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); - - // Asset - Assert.Equal(new[] { "Test", "Real Data" }, frontMatter.Params["Categories"]); - Assert.Equal(new[] { "Test", "Real Data" }, (frontMatter.Params["NestedData"] as Dictionary)?["Level2"]); - Assert.Equal("Test", ((frontMatter.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenSiteIsNull() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!, "fakeFilePath")); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathIsNull() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!)); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile("fakePath")); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist2() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, null!, "fakeContent")); - } - - [Fact] - public void ParseFrontmatter_ShouldHandleEmptyFileContent() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "")); - } - - [Fact] - public void ParseYAML_ShouldThrowExceptionWhenFrontmatterIsInvalid() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "invalidFrontmatter")); - } - - [Fact] - public void ParseSiteSettings_ShouldReturnSiteSettings() - { - var site = parser.ParseSiteSettings(siteContentCONST); - Assert.NotNull(site); - Assert.Equal("My Site", site.Title); - Assert.Equal("https://www.example.com/", site.BaseURL); - } - - [Fact] - public void ParseSiteSettings_ShouldReturnContent() - { - var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); - - Assert.Equal(pageMarkdownCONST, frontMatter.RawContent); - } - - [Fact] - public void SiteParams_ShouldThrowExceptionWhenSettingsIsNull() - { - _ = Assert.Throws(() => parser.ParseParams(null!, typeof(Site), siteContentCONST)); - } - - [Fact] - public void SiteParams_ShouldThrowExceptionWhenTypeIsNull() - { - _ = Assert.Throws(() => parser.ParseParams(site, null!, siteContentCONST)); - } - - [Fact] - public void SiteParams_ShouldHandleEmptyContent() - { - parser.ParseParams(site, typeof(Site), string.Empty); - Assert.Empty(site.Params); - } - - [Fact] - public void SiteParams_ShouldPopulateParamsWithExtraFields() - { - parser.ParseParams(site, typeof(Site), siteContentCONST); - Assert.NotEmpty(site.Params); - Assert.True(site.Params.ContainsKey("customParam")); - Assert.Equal("Custom Value", site.Params["customParam"]); - Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)site.Params["NestedData"])["Level2"]); - Assert.Equal("Test", ((site.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); - } + // Assert + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent)); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() + { + // Act + var siteSettings = parser.ParseSiteSettings(siteContentCONST); + + + // Assert + Assert.Equal("My Site", siteSettings.Title); + Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + Assert.Equal("Tastiest C# Static Site Generator of the World", siteSettings.Description); + Assert.Equal("Copyright message", siteSettings.Copyright); + } + + [Fact] + public void ParseParams_ShouldFillParamsWithNonMatchingFields() + { + // Arrange + var page = new Page(parser.ParseFrontmatterAndMarkdown(string.Empty, string.Empty, pageContent), site); + + // Assert + Assert.False(page.Params.ContainsKey("customParam")); + Assert.Equal("Custom Value", page.Params["ParamsCustomParam"]); + } + + [Fact] + public void ParseFrontmatter_ShouldParseContentInSiteFolder() + { + // Arrange + var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); + Page page = new(frontMatter, site); + + // Act + site.PostProcessPage(page); + + // Assert + Assert.Equal(date, frontMatter.Date); + } + + [Fact] + public void ParseFrontmatter_ShouldCreateTags() + { + // Arrange + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); + Page page = new(frontMatter, site); + + // Act + site.PostProcessPage(page); + + // Assert + Assert.Equal(2, page.TagsReference.Count); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenSiteIsNull() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!, "fakeFilePath")); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathIsNull() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!)); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile("fakePath")); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist2() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, null!, "fakeContent")); + } + + [Fact] + public void ParseFrontmatter_ShouldHandleEmptyFileContent() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "")); + } + + [Fact] + public void ParseYAML_ShouldThrowExceptionWhenFrontmatterIsInvalid() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "invalidFrontmatter")); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnSiteSettings() + { + // Arrange + var siteSettings = parser.ParseSiteSettings(siteContentCONST); + + // Assert + Assert.NotNull(siteSettings); + Assert.Equal("My Site", siteSettings.Title); + Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnContent() + { + // Arrange + var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); + + // Assert + Assert.Equal(pageMarkdownCONST, frontMatter.RawContent); + } + + + [Fact] + public void SiteParams_ShouldHandleEmptyContent() + { + Assert.Empty(site.Params); + } + + [Fact] + public void SiteParams_ShouldPopulateParamsWithExtraFields() + { + // Arrange + var siteSettings = parser.ParseSiteSettings(siteContentCONST); + site = new Site(generateOptionsMock, siteSettings, frontMatterParser, loggerMock, systemClockMock); + + // Assert + Assert.NotEmpty(siteSettings.Params); + Assert.DoesNotContain("customParam", site.Params); + Assert.Contains("ParamsCustomParam", site.Params); + Assert.Equal("Custom Value", site.Params["ParamsCustomParam"]); + Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)siteSettings.Params["ParamsNestedData"])["Level2"]); + Assert.Equal("Test", ((siteSettings.Params["ParamsNestedData"] as Dictionary)?["Level2"] as List)?[0]); + } } diff --git a/test/TestSetup.cs b/test/TestSetup.cs index aa0f4d0..02304da 100644 --- a/test/TestSetup.cs +++ b/test/TestSetup.cs @@ -23,7 +23,7 @@ public class TestSetup protected const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; protected const string testSitePathCONST08 = ".TestSites/08-theme-html"; - protected readonly IFrontMatterParser frontMatterParser = new YAMLParser(); + protected readonly IFrontMatterParser frontMatterParser = new SuCoS.Parser.YAMLParser(); protected readonly IGenerateOptions generateOptionsMock = Substitute.For(); protected readonly SiteSettings siteSettingsMock = Substitute.For(); protected readonly ILogger loggerMock = Substitute.For(); @@ -34,7 +34,7 @@ public class TestSetup SourceRelativePath = sourcePathCONST }; - protected readonly ISite site; + protected ISite site; // based on the compiled test.dll path // that is typically "bin/Debug/netX.0/test.dll" @@ -45,4 +45,10 @@ public class TestSetup _ = systemClockMock.Now.Returns(todayDate); site = new Site(generateOptionsMock, siteSettingsMock, frontMatterParser, loggerMock, systemClockMock); } + + public TestSetup(SiteSettings siteSettings) + { + _ = systemClockMock.Now.Returns(todayDate); + site = new Site(generateOptionsMock, siteSettings, frontMatterParser, loggerMock, systemClockMock); + } } -- GitLab From c673a9a274c47b41695e02dcd2b0c8d1f0df7a4f Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 9 Apr 2024 01:10:11 -0500 Subject: [PATCH 6/6] chore: rename IFrontMatterParser to IMetadataParser --- source/BaseGeneratorCommand.cs | 2 +- source/Helpers/SiteHelper.cs | 4 +- source/Models/Site.cs | 4 +- ...rontmatterParser.cs => IMetadataParser.cs} | 4 +- source/Parser/ParamsConverter.cs | 98 +++++++++++++++ source/Parser/YAMLParser.cs | 114 +----------------- test/TestSetup.cs | 2 +- 7 files changed, 108 insertions(+), 120 deletions(-) rename source/Parser/{IFrontmatterParser.cs => IMetadataParser.cs} (91%) create mode 100644 source/Parser/ParamsConverter.cs diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index d70757e..31710dd 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -26,7 +26,7 @@ public abstract class BaseGeneratorCommand /// /// The front matter parser instance. The default is YAML. /// - protected IFrontMatterParser frontMatterParser { get; } = new YAMLParser(); + protected IMetadataParser frontMatterParser { get; } = new YAMLParser(); /// /// The stopwatch reporter. diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index a6a6d37..06dd373 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -25,7 +25,7 @@ public static class SiteHelper /// Creates the pages dictionary. /// /// - public static Site Init(string configFile, IGenerateOptions options, IFrontMatterParser frontMatterParser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch) + public static Site Init(string configFile, IGenerateOptions options, IMetadataParser frontMatterParser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch) { ArgumentNullException.ThrowIfNull(stopwatch); @@ -92,7 +92,7 @@ public static class SiteHelper /// The front matter parser. /// The site settings file. /// The site settings. - private static SiteSettings ParseSettings(string configFile, IGenerateOptions options, IFrontMatterParser frontMatterParser) + private static SiteSettings ParseSettings(string configFile, IGenerateOptions options, IMetadataParser frontMatterParser) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(frontMatterParser); diff --git a/source/Models/Site.cs b/source/Models/Site.cs index f6f26d7..81162ff 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -166,7 +166,7 @@ public class Site : ISite /// /// The front matter parser instance. The default is YAML. /// - private readonly IFrontMatterParser frontMatterParser; + private readonly IMetadataParser frontMatterParser; private IEnumerable? pagesCache; @@ -185,7 +185,7 @@ public class Site : ISite public Site( in IGenerateOptions options, in SiteSettings settings, - in IFrontMatterParser frontMatterParser, + in IMetadataParser frontMatterParser, in ILogger logger, ISystemClock? clock) { Options = options; diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IMetadataParser.cs similarity index 91% rename from source/Parser/IFrontmatterParser.cs rename to source/Parser/IMetadataParser.cs index 67a536c..959e84c 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IMetadataParser.cs @@ -3,9 +3,9 @@ using SuCoS.Models; namespace SuCoS.Parser; /// -/// Responsible for parsing the content front matter +/// Responsible for parsing the content metadata /// -public interface IFrontMatterParser +public interface IMetadataParser { /// /// Extract the front matter from the content file. diff --git a/source/Parser/ParamsConverter.cs b/source/Parser/ParamsConverter.cs new file mode 100644 index 0000000..952b3ea --- /dev/null +++ b/source/Parser/ParamsConverter.cs @@ -0,0 +1,98 @@ +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace SuCoS.Parser; + +/// +/// A custom YAML type converter for dictionaries with string keys and object values. +/// +public class ParamsConverter : IYamlTypeConverter +{ + /// + /// Checks if the converter can handle the specified type. + /// + /// The type to check. + /// True if the type is a dictionary with string keys and object values, false otherwise. + public bool Accepts(Type type) + { + return type == typeof(Dictionary); + } + + /// + /// Reads a YAML stream and deserializes it into a dictionary. + /// + /// The YAML parser. + /// The type of the object to deserialize. + /// A dictionary deserialized from the YAML stream. + public object ReadYaml(IParser parser, Type type) + { + var dictionary = new Dictionary(); + + if (!parser.TryConsume(out _)) + { + // throw new YamlException("Expected a mapping start."); + } + + while (!parser.TryConsume(out _)) + { + if (!parser.TryConsume(out var key)) + { + throw new YamlException("Expected a key."); + } + + if (parser.TryConsume(out _)) + { + var list = new List(); + while (!parser.TryConsume(out _)) + { + if (parser?.Current is MappingStart) + { + list.Add(ReadYaml(parser, type)); + } + else if (parser?.Current is Scalar) + { + if (parser.TryConsume(out var scalar)) + { + list.Add(scalar.Value); + } + } + else + { + throw new YamlException( + "Expected a value, a nested mapping, or a sequence end." + ); + } + } + + dictionary[key.Value] = list; + } + else if (parser.TryConsume(out _)) + { + var nestedDictionary = + (Dictionary)ReadYaml(parser, type); + dictionary[key.Value] = nestedDictionary; + } + else + { + if (parser.TryConsume(out var value)) + { + dictionary[key.Value] = value.Value; + } + } + } + + return dictionary; + } + + /// + /// Writes an object to a YAML stream. + /// + /// The YAML emitter. + /// The object to serialize. + /// The type of the object to serialize. + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } +} diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 43f7954..d37c554 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -1,8 +1,6 @@ using System.Text; using SuCoS.Helpers; using SuCoS.Models; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; using YamlDotNet.Serialization; namespace SuCoS.Parser; @@ -10,29 +8,22 @@ namespace SuCoS.Parser; /// /// Responsible for parsing the content front matter using YAML /// -public class YAMLParser : IFrontMatterParser +public class YAMLParser : IMetadataParser { /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields /// private readonly IDeserializer yamlDeserializerRigid; - private readonly StaticAOTContext staticAOTContext; - private readonly ISerializer yamlSerializer; - /// /// ctor /// public YAMLParser() { - staticAOTContext = new(); - - yamlDeserializerRigid = new StaticDeserializerBuilder(staticAOTContext) + yamlDeserializerRigid = new StaticDeserializerBuilder(new StaticAOTContext()) .WithTypeConverter(new ParamsConverter()) .IgnoreUnmatchedProperties() .Build(); - - yamlSerializer = new StaticSerializerBuilder(staticAOTContext).Build(); } /// @@ -111,14 +102,6 @@ public class YAMLParser : IFrontMatterParser frontMatter.SourceRelativePath = fileRelativePath; frontMatter.SourceFullPath = fileFullPath; frontMatter.Type ??= section; - - // var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml)); - // if (yamlObject is not Dictionary yamlDictionary) - // { - // return frontMatter; - // } - // ParseParams(frontMatter, typeof(Page), yaml, yamlDictionary); - return frontMatter; } @@ -129,96 +112,3 @@ public class YAMLParser : IFrontMatterParser return settings; } } - -/// -/// A custom YAML type converter for dictionaries with string keys and object values. -/// -public class ParamsConverter : IYamlTypeConverter -{ - /// - /// Checks if the converter can handle the specified type. - /// - /// The type to check. - /// True if the type is a dictionary with string keys and object values, false otherwise. - public bool Accepts(Type type) - { - return type == typeof(Dictionary); - } - - /// - /// Reads a YAML stream and deserializes it into a dictionary. - /// - /// The YAML parser. - /// The type of the object to deserialize. - /// A dictionary deserialized from the YAML stream. - public object ReadYaml(IParser parser, Type type) - { - var dictionary = new Dictionary(); - - if (!parser.TryConsume(out _)) - { - // throw new YamlException("Expected a mapping start."); - } - - while (!parser.TryConsume(out _)) - { - if (!parser.TryConsume(out var key)) - { - throw new YamlException("Expected a key."); - } - - if (parser.TryConsume(out _)) - { - var list = new List(); - while (!parser.TryConsume(out _)) - { - if (parser?.Current is MappingStart) - { - list.Add(ReadYaml(parser, type)); - } - else if (parser?.Current is Scalar) - { - if (parser.TryConsume(out var scalar)) - { - list.Add(scalar.Value); - } - } - else - { - throw new YamlException( - "Expected a value, a nested mapping, or a sequence end." - ); - } - } - - dictionary[key.Value] = list; - } - else if (parser.TryConsume(out _)) - { - var nestedDictionary = - (Dictionary)ReadYaml(parser, type); - dictionary[key.Value] = nestedDictionary; - } - else - { - if (parser.TryConsume(out var value)) - { - dictionary[key.Value] = value.Value; - } - } - } - - return dictionary; - } - - /// - /// Writes an object to a YAML stream. - /// - /// The YAML emitter. - /// The object to serialize. - /// The type of the object to serialize. - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - throw new NotImplementedException(); - } -} diff --git a/test/TestSetup.cs b/test/TestSetup.cs index 02304da..ce5d9b9 100644 --- a/test/TestSetup.cs +++ b/test/TestSetup.cs @@ -23,7 +23,7 @@ public class TestSetup protected const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; protected const string testSitePathCONST08 = ".TestSites/08-theme-html"; - protected readonly IFrontMatterParser frontMatterParser = new SuCoS.Parser.YAMLParser(); + protected readonly IMetadataParser frontMatterParser = new SuCoS.Parser.YAMLParser(); protected readonly IGenerateOptions generateOptionsMock = Substitute.For(); protected readonly SiteSettings siteSettingsMock = Substitute.For(); protected readonly ILogger loggerMock = Substitute.For(); -- GitLab