Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make changes to annotation targets describing paths concluding with a navigation property #269

Merged
merged 8 commits into from
Apr 18, 2024
112 changes: 29 additions & 83 deletions ApiDoctor.Publishing/CSDL/csdlwriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,18 +425,18 @@ public EntityFramework CreateEntityFrameworkFromDocs(IssueLogger issues, string
var prefix = schema.Namespace + "." + container.Name;
foreach (var entitySet in container.EntitySets)
{
var annotationName = prefix + "/" + entitySet.Name;
var annotationTarget = prefix + "/" + entitySet.Name;
AddLinkAndRestrictionAnnotations(edmx, annotationsMap, entitySet,
entitySet.SourceMethods as MethodCollection,
annotationName, issues.For(annotationName));
annotationTarget, issues.For(annotationTarget));
}

foreach (var singleton in container.Singletons)
{
var annotationName = prefix + "/" + singleton.Name;
var annotationTarget = prefix + "/" + singleton.Name;
AddLinkAndRestrictionAnnotations(edmx, annotationsMap, singleton,
singleton.SourceMethods as MethodCollection,
annotationName, issues.For(annotationName));
annotationTarget, issues.For(annotationTarget));
}
}
schema.Annotations = annotationsMap.Values.ToList();
Expand Down Expand Up @@ -638,29 +638,22 @@ private static void MergeAnnotations(string fullName, IODataAnnotatable annotata
{
if (annotatable.Annotation?.Count > 0)
{
Annotations annotations;
if (schemaLevelAnnotations.TryGetValue(fullName, out annotations))
if (schemaLevelAnnotations.TryGetValue(fullName, out Annotations targetAnnotations))
{
foreach (var annotation in annotatable.Annotation)
{
var existingAnnotation = annotations.AnnotationList.FirstOrDefault(a => a.Term == annotation.Term);
var existingAnnotation = targetAnnotations.AnnotationList.FirstOrDefault(a => a.Term == annotation.Term);
if (existingAnnotation != null && annotation.Collection != null && annotation.Collection.Records?.Count > 0)
{
if (existingAnnotation.Collection == null)
{
existingAnnotation.Collection = new RecordCollection();
}
existingAnnotation.Collection ??= new RecordCollection();
millicentachieng marked this conversation as resolved.
Show resolved Hide resolved

if (existingAnnotation.Collection.Records == null)
{
existingAnnotation.Collection.Records = new List<Record>();
}
existingAnnotation.Collection.Records ??= new List<Record>();

existingAnnotation.Collection.Records.AddRange(annotation.Collection.Records);
}
else
{
annotations.AnnotationList.Add(annotation);
targetAnnotations.AnnotationList.Add(annotation);
}
}
}
Expand Down Expand Up @@ -875,7 +868,7 @@ private void CreateNewActionOrFunction(
}
}

if (target.Name.Contains("("))
if (target.Name.Contains('('))
{
target.ParameterizedName = target.Name;
var pathParameters = target.Name.TextBetweenCharacters('(', ')').Split(',');
Expand Down Expand Up @@ -986,7 +979,7 @@ private void CreateNewActionOrFunction(
/// <returns></returns>
private static ODataTargetInfo ParseRequestTargetType(string requestPath, EntityFramework edmx, IssueLogger issues)
{
string[] requestParts = requestPath.Substring(1).Split(new char[] { '/' });
string[] requestParts = requestPath[1..].Split(['/']);

EntityContainer entryPoint = (from s in edmx.DataServices.Schemas
where s.EntityContainers.Count > 0
Expand Down Expand Up @@ -1094,7 +1087,7 @@ where s.EntityContainers.Count > 0
private static readonly System.Text.RegularExpressions.Regex EntitySetPathRegEx = new(@"^\/(\w*)\/{var}$", System.Text.RegularExpressions.RegexOptions.Compiled);
// Singleton is something in the format of /name or /root/child/subChild where root is the singleton
private static readonly System.Text.RegularExpressions.Regex SingletonPathRegEx = new(@"^\/(\w*)$", System.Text.RegularExpressions.RegexOptions.Compiled);
private static readonly System.Text.RegularExpressions.Regex FullPathRegEx = new(@"\/(\w+)", System.Text.RegularExpressions.RegexOptions.Compiled);
private static readonly System.Text.RegularExpressions.Regex FullPathRegEx = new(@"\/([\w\.]+)", System.Text.RegularExpressions.RegexOptions.Compiled);

/// <summary>
/// Parse the URI paths for methods defined in the documentation and construct an entity container that contains these
Expand Down Expand Up @@ -1632,7 +1625,7 @@ private void AddSourceMethods(EntityFramework edmx, IssueLogger issues)
private static ActionOrFunctionBase FindActionOrFunctionTarget(string path, EntityFramework edmx, IssueLogger issues)
{
ActionOrFunctionBase actionOrFunction = null;
string[] requestParts = path.Substring(1).Split(new char[] { '/' });
string[] requestParts = path[1..].Split(['/']);

if (requestParts.Length < 2 || requestParts.Last() == "{var}")
return null;
Expand All @@ -1654,13 +1647,15 @@ private static ActionOrFunctionBase FindActionOrFunctionTarget(string path, Enti
if (requestTarget.QualifiedType.IsCollection())
bindingParameterType = $"Collection({bindingParameterType})";

int bracketStartIndex = requestParts[^1].IndexOf('(');
var actionOrFunctionName = bracketStartIndex == -1 ? requestParts[^1] : requestParts[^1][..bracketStartIndex];
var matches = actionsAndFunctions.Where(x =>
x.IsBound &&
(x.Name.IEquals(requestParts.Last()) || x.ParameterizedName.IEquals(requestParts.Last())) &&
(x.Name.IEquals(actionOrFunctionName) || x.ParameterizedName.IEquals(requestParts.Last())) &&
x.Parameters.Any(x => x.Type == bindingParameterType))
.ToList();

if (matches.Any())
if (matches.Count != 0)
{
actionOrFunction = matches.Where(x => x.Parameters.Any(x => x.Name == "bindingParameter")).FirstOrDefault();
if (actionOrFunction == null && matches.Count == 1) // bindingParameter not specified but can be deduced
Expand All @@ -1675,48 +1670,8 @@ private static ActionOrFunctionBase FindActionOrFunctionTarget(string path, Enti
{
issues.Error(ValidationErrorCode.Unknown, path, ex);
}
return actionOrFunction;
}

private static string FindNavigationPropertyTarget(string path, EntityFramework edmx, IssueLogger issues)
{
var requestParts = path.Substring(1).Split(new char[] { '/' }).ToList();

if (requestParts.Count < 2)
return null;
try
{
if (requestParts.Last() == "{var}")
requestParts.RemoveAt(requestParts.Count - 1);

var targetPath = string.Join('/', requestParts.SkipLast(1));
ODataTargetInfo requestTarget = ParseRequestTargetType($"/{targetPath}", edmx, issues);

if (requestTarget?.QualifiedType != null)
{
var requestTargetNamespace = requestTarget.QualifiedType.NamespaceOnly();
var schema = edmx.DataServices.Schemas.FirstOrDefault(x => x.Namespace == requestTargetNamespace || x.Alias == requestTargetNamespace);
if (schema == null)
throw new Exception($"Unknown namespace: {requestTargetNamespace}");

var typeOnly = requestTarget.QualifiedType.TypeOnly();
var complexType = schema.ComplexTypes.Concat(schema.EntityTypes)
.Where(x => x.Name == typeOnly).FirstOrDefault();
if (complexType != null)
{
var navProperty = complexType.NavigateByUriComponent(requestParts.Last(), edmx, issues, true);
if (navProperty != null)
{
return $"{schema.Namespace}.{typeOnly}/{requestParts.Last()}";
}
}
}
}
catch (Exception ex)
{
issues.Error(ValidationErrorCode.Unknown, path, ex);
}
return null;
return actionOrFunction;
}

private static void AddLinkAndRestrictionAnnotations(EntityFramework edmx, Dictionary<string, Annotations> annotationsMap, ISet set, MethodCollection methodCollection, string target, IssueLogger issues)
Expand Down Expand Up @@ -1783,20 +1738,11 @@ private static Dictionary<string, List<IODataAnnotatable>> GenerateLinkAndRestri
requestTargetMapping.TryGetValue(url, out var requestTargetName);
if (requestTargetName == null)
{
var navPropertyTargetName = FindNavigationPropertyTarget(url, edmx, issues);
if (navPropertyTargetName != null)
{
path = navPropertyTargetName;
requestTargetMapping.Add(url, navPropertyTargetName);
}
ActionOrFunctionBase actionOrFunction = FindActionOrFunctionTarget(url, edmx, issues);
if (actionOrFunction != null)
annotatable = actionOrFunction;
else
{
ActionOrFunctionBase actionOrFunction = FindActionOrFunctionTarget(url, edmx, issues);
if (actionOrFunction != null)
{
annotatable = actionOrFunction;
}
}
requestTargetMapping.Add(url, path);
}
else
{
Expand Down Expand Up @@ -1940,10 +1886,10 @@ private static string GetRestrictionTermForMethod(string httpMethod)


#region Links Annotations
private static string CreateHrefValue(string sourceFilePath)
private static string CreateHrefValue(MethodDefinition method)
{
sourceFilePath = sourceFilePath.Remove(sourceFilePath.Length - 3); // remove .md file extension
var version = sourceFilePath.Contains("beta") ? "beta" : "1.0";
var sourceFilePath = method.SourceFile.DisplayName[..^3]; // remove .md file extension
var version = method.SourceFile.FullPath.Contains("beta") ? "beta" : "1.0";

var uriBuilder = new UriBuilder()
{
Expand All @@ -1955,7 +1901,7 @@ private static string CreateHrefValue(string sourceFilePath)
return uriBuilder.ToString();
}

private static Record CreateLinksRecord(string sourceFilePath, string linkRel)
private static Record CreateLinksRecord(MethodDefinition method, string linkRel)
{
return new Record
{
Expand All @@ -1969,7 +1915,7 @@ private static Record CreateLinksRecord(string sourceFilePath, string linkRel)
new()
{
Property = "href",
String = CreateHrefValue(sourceFilePath)
String = CreateHrefValue(method)
}
}
};
Expand Down Expand Up @@ -2002,7 +1948,7 @@ private static void AddLinkAnnotation(IODataAnnotatable annotatable, MethodDefin
var linkRel = GetLinkRelValueForMethod(annotatable, method.HttpMethodVerb());
if (linkRel == null) return;

var linkRecord = CreateLinksRecord(method.SourceFile.DisplayName, linkRel);
var linkRecord = CreateLinksRecord(method, linkRel);

annotatable.Annotation ??= new List<Annotation>();
var linkAnnotation = annotatable.Annotation.FirstOrDefault(x => x.Term == Term.LinksTerm);
Expand Down Expand Up @@ -2036,7 +1982,7 @@ private static void AddReadByKeyLinkAnnotation(IODataAnnotatable annotatable, Me
{
var readRestriction = annotatable.Annotation
.FirstOrDefault(annotation => annotation.Term == Term.LinksTerm);
var currentLinkRecord = CreateLinksRecord(sourceMethod.SourceFile.DisplayName, Term.LinkRel.ReadByKey);
var currentLinkRecord = CreateLinksRecord(sourceMethod, Term.LinkRel.ReadByKey);
if (readRestriction != null)
{
var propertyValue = readRestriction.Collection.Records
Expand Down
7 changes: 5 additions & 2 deletions ApiDoctor.Validation/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static class ExtensionMethods

private static readonly Regex likelyBase64Regex = new("^[a-fA-F0-9+=/]+$", RegexOptions.Compiled);

private static readonly Regex markdownLinkRegex = new(@"\[(.*?)\]((\(.*?\))|(\s*:\s*\w+))", RegexOptions.Compiled);
private static readonly Regex markdownLinkRegex = new(@"\[(.*?)\]((\(.*?\))|(\[.*?\])|(\s*:\s*\w+))", RegexOptions.Compiled);

private static readonly Regex markdownBoldRegex = new(@"\*\*(.*?)\*\*", RegexOptions.Compiled);

Expand Down Expand Up @@ -243,9 +243,12 @@ public static string RemoveMarkdownStyling(this string markdownText)
// Remove code (`code`)
markdownText = markdownCodeRegex.Replace(markdownText, "$1");

// Remove links [text](link_target) or [text]: link_reference
// Remove links [text](link_target) or [text]: link_reference or [text][] or [text][link_reference]
markdownText = markdownLinkRegex.Replace(markdownText, "$1");

// Remove line break character
markdownText = markdownText.Replace("&#xA;", " ");

return markdownText;
}

Expand Down
Loading