Skip to content

Commit

Permalink
Validate document structure (#249)
Browse files Browse the repository at this point in the history
* Add extension method to check string distance between two strings

* Add extension method for parsing JSON

* Add new validation error codes

* Configure fetching of document structure JSON config files

* Validate docs structure against config based on doc type

* Use Fastenshtein library to check string distance

* Optimize code for validating doc structure

* Update launch.json

* Remove unused code

* Add null check

* Add check for collection size

* Update custom JSON deserializer
  • Loading branch information
millicentachieng authored Mar 25, 2024
1 parent 1708be5 commit 1916d8c
Show file tree
Hide file tree
Showing 11 changed files with 564 additions and 196 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
"processId": "${command:pickProcess}"
}
]
}
}
2 changes: 1 addition & 1 deletion ApiDoctor.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ private static Task<DocSet> GetDocSetAsync(DocSetOptions options, IssueLogger is
DateTimeOffset end = DateTimeOffset.Now;
TimeSpan duration = end.Subtract(start);

FancyConsole.WriteLine($"Took {duration.TotalSeconds} to parse {set.Files.Length} source files.");
FancyConsole.WriteLine($"Took {duration.TotalSeconds}s to parse {set.Files.Length} source files.");
return Task.FromResult<DocSet>(set);
}

Expand Down
1 change: 1 addition & 0 deletions ApiDoctor.Validation/ApiDoctor.Validation.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ProjectReference Include="..\OSS\markdowndeep\MarkdownDeep\MarkdownDeep.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fastenshtein" Version="1.0.0.8" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.CodeDom" Version="8.0.0" />
Expand Down
2 changes: 1 addition & 1 deletion ApiDoctor.Validation/Config/ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public abstract class ConfigFile
public string SourcePath { get; set; }

/// <summary>
/// Provide oppertunity to post-process a valid configuration after the file is loaded.
/// Provide opportunity to post-process a valid configuration after the file is loaded.
/// </summary>
public virtual void LoadComplete()
{
Expand Down
215 changes: 153 additions & 62 deletions ApiDoctor.Validation/Config/DocumentOutlineFile.cs
Original file line number Diff line number Diff line change
@@ -1,93 +1,184 @@
/*
* API Doctor
* Copyright (c) Microsoft Corporation
* 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,
* 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,
* subject to the following conditions:
*
* 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
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
* API Doctor
* Copyright (c) Microsoft Corporation
* 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,
* 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,
* subject to the following conditions:
*
* 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
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

namespace ApiDoctor.Validation.Config
{
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;

public class DocumentOutlineFile : ConfigFile
{
[JsonProperty("allowedDocumentHeaders")]
public DocumentHeader[] AllowedHeaders { get; set; }
[JsonProperty("apiPageType"), JsonConverter(typeof(DocumentHeaderJsonConverter))]
public List<object> ApiPageType { get; set; } = [];

[JsonProperty("resourcePageType"), JsonConverter(typeof(DocumentHeaderJsonConverter))]
public List<object> ResourcePageType { get; set; } = [];

[JsonProperty("conceptualPageType"), JsonConverter(typeof(DocumentHeaderJsonConverter))]
public List<object> ConceptualPageType { get; set; } = [];

[JsonProperty("enumPageType"), JsonConverter(typeof(DocumentHeaderJsonConverter))]
public List<object> EnumPageType { get; set; } = [];

public override bool IsValid => ApiPageType.Count > 0 || ResourcePageType.Count > 0 || ConceptualPageType.Count > 0 || EnumPageType.Count > 0;
}


public class ExpectedDocumentHeader : DocumentHeader
{
/// <summary>
/// Indicates that a header pattern can be repeated multiple times e.g. in the case of multiple examples
/// </summary>
[JsonProperty("allowMultiple")]
public bool AllowMultiple { get; set; }

public override bool IsValid
/// <summary>
/// Specifies the headers that are allowed under this header.
/// </summary>
[JsonProperty("headers"), JsonConverter(typeof(DocumentHeaderJsonConverter))]
public new List<object> ChildHeaders { get; set; } = [];

public ExpectedDocumentHeader() { }

public ExpectedDocumentHeader(ExpectedDocumentHeader original) : base(original)
{
get
if (original == null)
return;

AllowMultiple = original.AllowMultiple;

ChildHeaders = CopyHeaders(original.ChildHeaders);
}

public static List<object> CopyHeaders(List<object> headers)
{
if (headers == null)
return null;

var headersCopy = new List<object>();
foreach (var header in headers)
{
return this.AllowedHeaders != null;
headersCopy.Add(header switch
{
ConditionalDocumentHeader conditionalDocHeader => new ConditionalDocumentHeader(conditionalDocHeader),
ExpectedDocumentHeader expectedDocHeader => new ExpectedDocumentHeader(expectedDocHeader),
_ => throw new InvalidOperationException("Unexpected header type")
});
}
return headersCopy;
}
}

public class DocumentHeader
public class ConditionalDocumentHeader
{
public DocumentHeader()
[JsonProperty("condition")]
public string Condition { get; set; }

[JsonProperty("arguments"), JsonConverter(typeof(DocumentHeaderJsonConverter))]
public List<object> Arguments { get; set; }

public ConditionalOperator? Operator
{
Level = 1;
ChildHeaders = new List<DocumentHeader>();
get
{
return Enum.TryParse(this.Condition, true, out ConditionalOperator op) ? op : null;
}
}

/// <summary>
/// Represents the header level using markdown formatting (1=#, 2=##, 3=###, 4=####, 5=#####, 6=######)
/// </summary>
[JsonProperty("level")]
public int Level { get; set; }
public ConditionalDocumentHeader(ConditionalDocumentHeader original)
{
if (original == null)
return;

/// <summary>
/// Indicates that a header at this level is required.
/// </summary>
[JsonProperty("required")]
public bool Required { get; set; }
Condition = original.Condition;

/// <summary>
/// The expected value of a title or empty to indicate any value
/// </summary>
[JsonProperty("title")]
public string Title { get; set; }
Arguments = ExpectedDocumentHeader.CopyHeaders(original.Arguments);
}
}

/// <summary>
/// Specifies the headers that are allowed under this header.
/// </summary>
[JsonProperty("headers")]
public List<DocumentHeader> ChildHeaders { get; set; }
public enum ConditionalOperator
{
OR,
AND
}

public class DocumentHeaderJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ExpectedDocumentHeader) || objectType == typeof(ConditionalDocumentHeader);
}

internal bool Matches(DocumentHeader found)
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return this.Level == found.Level && DoTitlesMatch(this.Title, found.Title);
if (reader.TokenType == JsonToken.StartArray)
{
var allowedHeaders = new List<object>();
var jArray = JArray.Load(reader);
foreach (var item in jArray)
{
if (item is JObject jObject)
{
object header;
if (jObject.ContainsKey("condition"))
{
header = jObject.ToObject<ConditionalDocumentHeader>(serializer);
}
else if (jObject.ContainsKey("title"))
{
header = jObject.ToObject<ExpectedDocumentHeader>(serializer);
}
else
{
throw new JsonReaderException("Invalid document header definition");
}
allowedHeaders.Add(header);
}
else
{
throw new JsonReaderException("Invalid document header definition");
}
}
return allowedHeaders;
}
else if (reader.TokenType == JsonToken.Null)
{
return null;
}
else
{
throw new JsonSerializationException($"Unexpected token: {existingValue}");
}
}

private static bool DoTitlesMatch(string expectedTitle, string foundTitle)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (expectedTitle == foundTitle) return true;
if (string.IsNullOrEmpty(expectedTitle) || expectedTitle == "*") return true;
if (expectedTitle.StartsWith("* ") && foundTitle.EndsWith(expectedTitle.Substring(2))) return true;
if (expectedTitle.EndsWith(" *") && foundTitle.StartsWith(expectedTitle.Substring(0, expectedTitle.Length - 2))) return true;
return false;
serializer.Serialize(writer, value);
}
}

}
}
Loading

0 comments on commit 1916d8c

Please sign in to comment.