From 59f67ac6ba063f8bbdc8d458299eaf68c7a46b1d Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 6 Jun 2024 14:47:50 -0400 Subject: [PATCH] Refactored YAML parsing - Replaced custom string parsing with YamlDotNet deserializer - Added test for malformed YAML --- .vscode/settings.json | 3 + .../YamlParserTests.cs | 31 ++++++++ ApiDoctor.Validation/DocFile.cs | 71 +++++++++---------- 3 files changed, 68 insertions(+), 37 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ef927836 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "ApiDoctor.sln" +} diff --git a/ApiDoctor.Validation.UnitTests/YamlParserTests.cs b/ApiDoctor.Validation.UnitTests/YamlParserTests.cs index 938af34e..96c4a11d 100644 --- a/ApiDoctor.Validation.UnitTests/YamlParserTests.cs +++ b/ApiDoctor.Validation.UnitTests/YamlParserTests.cs @@ -25,6 +25,7 @@ namespace ApiDoctor.Validation.UnitTests { + using System.Linq; using ApiDoctor.Validation; using ApiDoctor.Validation.Error; using NUnit.Framework; @@ -41,6 +42,18 @@ public class YamlParserTests toc.keywords: - foo - bar +"; + + // Missing closing double-quote on title property + private static readonly string malformedYaml = @"title: ""Define the /me as singleton +description: ""These are things I had to add in the docs to make sure the Markdown-Scanner"" +ms.localizationpriority: medium +author: """" +ms.prod: """" +doc_type: conceptualPageType +toc.keywords: +- foo +- bar "; [Test] @@ -56,5 +69,23 @@ public void YamlWithMultiLineArrayParses() // Assert Assert.That(!issues.Issues.WereErrors()); } + + [Test] + public void MalformedYamlGeneratesError() + { + // Arrange + _ = new DocSet(); + var issues = new IssueLogger(); + + // Act + DocFile.ParseYamlMetadata(malformedYaml, issues); + + // Assert + Assert.That(issues.Issues.WereErrors()); + var error = issues.Issues.FirstOrDefault(); + Assert.That(error != null); + Assert.That(error.IsError); + Assert.That(error.Message == "Incorrect YAML header format"); + } } } \ No newline at end of file diff --git a/ApiDoctor.Validation/DocFile.cs b/ApiDoctor.Validation/DocFile.cs index 56e8c912..64bc8a11 100644 --- a/ApiDoctor.Validation/DocFile.cs +++ b/ApiDoctor.Validation/DocFile.cs @@ -1,25 +1,25 @@ /* * API Doctor * Copyright (c) Microsoft Corporation -* All rights reserved. -* +* All rights reserved. +* * MIT License -* -* Permission is hereby granted, free of charge, to any person obtaining a copy of -* this software and associated documentation files (the ""Software""), to deal in -* the Software without restriction, including without limitation the rights to use, +* +* Permission is hereby granted, free of charge, to any person obtaining a copy of +* this software and associated documentation files (the ""Software""), to deal in +* the Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -* Software, and to permit persons to whom the Software is furnished to do so, +* Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all +* +* The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* +* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -30,12 +30,13 @@ namespace ApiDoctor.Validation using System.Diagnostics; using System.IO; using System.Linq; + using ApiDoctor.Validation.Config; using ApiDoctor.Validation.Error; using ApiDoctor.Validation.TableSpec; using Tags; using MarkdownDeep; using Newtonsoft.Json; - using ApiDoctor.Validation.Config; + using YamlDotNet.Serialization; /// /// A documentation file that may contain one more resources or API methods @@ -50,6 +51,8 @@ public partial class DocFile private readonly List enums = new List(); private readonly List bookmarks = new List(); + private static readonly IDeserializer yamlDeserializer = new DeserializerBuilder().Build(); + protected bool HasScanRun; protected string BasePath; @@ -389,29 +392,23 @@ internal static (string YamlFrontMatter, string ProcessedContent) ParseAndRemove internal static void ParseYamlMetadata(string yamlMetadata, IssueLogger issues) { - Dictionary dictionary = new Dictionary(); - string[] items = yamlMetadata.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - foreach (string item in items) + Dictionary dictionary = null; + try { - try - { - string[] keyValue = item.Split(':'); - dictionary.Add(keyValue[0].Trim(), keyValue[1].Trim()); - } - catch (Exception) - { - issues.Error(ValidationErrorCode.IncorrectYamlHeaderFormat, $"Incorrect YAML header format after `{dictionary.Keys.Last()}`"); - } + dictionary = yamlDeserializer.Deserialize>(yamlMetadata); + } + catch (Exception) + { + issues.Error(ValidationErrorCode.IncorrectYamlHeaderFormat, "Incorrect YAML header format"); } List missingHeaders = new List(); foreach (var header in DocSet.SchemaConfig.RequiredYamlHeaders) { - string value; - if (dictionary.TryGetValue(header, out value)) + if (dictionary.TryGetValue(header, out object value) && value is string stringValue) { - value = value.Replace("\"", string.Empty); - if (string.IsNullOrWhiteSpace(value)) + stringValue = stringValue.Replace("\"", string.Empty); + if (string.IsNullOrWhiteSpace(stringValue)) { issues.Warning(ValidationErrorCode.RequiredYamlHeaderMissing, $"Missing value for YAML header: {header}"); } @@ -732,7 +729,7 @@ private void CheckDocumentHeaders(List expectedHeaders, IReadOnlyList expectedHeaders, IReadOnlyList expec private static List FlattenDocumentHeaderHierarchy(IReadOnlyList headers) { var mergedHeaders = new List(); - foreach (var header in headers) + foreach (var header in headers) { if (header is ExpectedDocumentHeader expectedHeader) { @@ -1002,7 +999,7 @@ private void AddHeaderToHierarchy(Stack headerStack, Block block } /// - /// Validates code snippets tab section. + /// Validates code snippets tab section. /// Checks: /// - No duplicated tabs /// - Existence of tab boundary at the end of tab group definition @@ -1224,7 +1221,7 @@ private void PostProcessEnums(List foundEnums, List !string.IsNullOrEmpty(e.MemberName) && !string.IsNullOrEmpty(e.TypeName))); - // if we thought it was a table of type EnumerationValues, it probably holds enum values. + // if we thought it was a table of type EnumerationValues, it probably holds enum values. // throw error if member name is null which could mean a wrong column name foreach (var enumType in foundEnums.Where(e => string.IsNullOrEmpty(e.MemberName) && !string.IsNullOrEmpty(e.TypeName)) .Select(x => new { x.TypeName, x.Namespace }).Distinct())