Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public override async Task<FetchedCrossLinks> Fetch()
return new FetchedCrossLinks
{
DeclaredRepositories = declaredRepositories,
LinkReferences = dictionary.ToFrozenDictionary()
LinkReferences = dictionary.ToFrozenDictionary(),
FromConfiguration = true
};
}

Expand Down
6 changes: 5 additions & 1 deletion src/Elastic.Markdown/CrossLinks/CrossLinkFetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ namespace Elastic.Markdown.CrossLinks;
public record FetchedCrossLinks
{
public required FrozenDictionary<string, LinkReference> LinkReferences { get; init; }

public required HashSet<string> DeclaredRepositories { get; init; }

public required bool FromConfiguration { get; init; }

public static FetchedCrossLinks Empty { get; } = new()
{
DeclaredRepositories = [],
LinkReferences = new Dictionary<string, LinkReference>().ToFrozenDictionary()
LinkReferences = new Dictionary<string, LinkReference>().ToFrozenDictionary(),
FromConfiguration = false
};
}

Expand Down
15 changes: 10 additions & 5 deletions src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public record LinkIndexEntry
public interface ICrossLinkResolver
{
Task<FetchedCrossLinks> FetchLinks();
bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri);
bool TryResolve(Action<string> errorEmitter, Action<string> warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri);
}

public class CrossLinkResolver(CrossLinkFetcher fetcher) : ICrossLinkResolver
Expand All @@ -48,8 +48,8 @@ public async Task<FetchedCrossLinks> FetchLinks()
return _crossLinks;
}

public bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
TryResolve(errorEmitter, _crossLinks, crossLinkUri, out resolvedUri);
public bool TryResolve(Action<string> errorEmitter, Action<string> warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
TryResolve(errorEmitter, warningEmitter, _crossLinks, crossLinkUri, out resolvedUri);

private static Uri BaseUri { get; } = new("https://docs-v3-preview.elastic.dev");

Expand All @@ -66,6 +66,7 @@ public FetchedCrossLinks UpdateLinkReference(string repository, LinkReference li

public static bool TryResolve(
Action<string> errorEmitter,
Action<string> warningEmitter,
FetchedCrossLinks fetchedCrossLinks,
Uri crossLinkUri,
[NotNullWhen(true)] out Uri? resolvedUri
Expand All @@ -82,7 +83,10 @@ public static bool TryResolve(
var declaredRepositories = fetchedCrossLinks.DeclaredRepositories;
if (!declaredRepositories.Contains(crossLinkUri.Scheme))
{
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links");
if (fetchedCrossLinks.FromConfiguration)
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links: '{crossLinkUri}'");
else
warningEmitter($"'{crossLinkUri.Scheme}' is not yet publishing to the links registry: '{crossLinkUri}'");
return false;
}

Expand Down Expand Up @@ -163,7 +167,8 @@ private static bool LookupLink(
return true;
}

errorEmitter($"'{lookupPath}' is not a valid link in the '{crossLinkUri.Scheme}' cross link repository.");
var linksJson = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{crossLinkUri.Scheme}/main/links.json";
errorEmitter($"'{lookupPath}' is not a valid link in the '{crossLinkUri.Scheme}' cross link index: {linksJson}.");
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ private static void ProcessCrossLink(LinkInline link, InlineProcessor processor,
if (url != null)
context.Build.Collector.EmitCrossLink(url);

if (context.CrossLinkResolver.TryResolve(s => processor.EmitError(link, s), uri, out var resolvedUri))
if (context.CrossLinkResolver.TryResolve(
s => processor.EmitError(link, s),
s => processor.EmitWarning(link, s),
uri, out var resolvedUri)
)
link.Url = resolvedUri.ToString();
}

Expand Down
22 changes: 14 additions & 8 deletions src/docs-assembler/Cli/InboundLinkCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ namespace Documentation.Assembler.Cli;

internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService)
{
private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger, githubActionsService);

private void AssignOutputLogger()
{
var log = logger.CreateLogger<Program>();
Expand All @@ -33,22 +35,26 @@ private void AssignOutputLogger()
public async Task<int> ValidateAllInboundLinks(Cancel ctx = default)
{
AssignOutputLogger();
return await new LinkIndexLinkChecker(logger).CheckAll(githubActionsService, ctx);
return await _linkIndexLinkChecker.CheckAll(ctx);
}

/// <summary> Validate all published cross_links in all published links.json files. </summary>
/// <param name="repository"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="ctx"></param>
[Command("validate")]
public async Task<int> ValidateRepoInboundLinks(string? repository = null, Cancel ctx = default)
public async Task<int> ValidateRepoInboundLinks(string? from = null, string? to = null, Cancel ctx = default)
{
AssignOutputLogger();
var fs = new FileSystem();
var root = fs.DirectoryInfo.New(Paths.Root.FullName);
repository ??= GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName;
if (repository == null)
throw new Exception("Unable to determine repository name");
return await new LinkIndexLinkChecker(logger).CheckRepository(githubActionsService, repository, ctx);
if (from == null && to == null)
{
from ??= GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName;
if (from == null)
throw new Exception("Unable to determine repository name");
}
return await _linkIndexLinkChecker.CheckRepository(from, to, ctx);
}

/// <summary>
Expand All @@ -66,6 +72,6 @@ public async Task<int> ValidateLocalLinkReference(string? file = null, Cancel ct
var repository = GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName
?? throw new Exception("Unable to determine repository name");

return await new LinkIndexLinkChecker(logger).CheckWithLocalLinksJson(githubActionsService, repository, file, ctx);
return await _linkIndexLinkChecker.CheckWithLocalLinksJson(repository, file, ctx);
}
}
3 changes: 2 additions & 1 deletion src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public override async Task<FetchedCrossLinks> Fetch()
return new FetchedCrossLinks
{
DeclaredRepositories = declaredRepositories,
LinkReferences = dictionary.ToFrozenDictionary()
LinkReferences = dictionary.ToFrozenDictionary(),
FromConfiguration = false
};
}
}
55 changes: 40 additions & 15 deletions src/docs-assembler/Links/LinkIndexLinkChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,44 @@

namespace Documentation.Assembler.Links;

public class LinkIndexLinkChecker(ILoggerFactory logger)
public class LinkIndexLinkChecker(ILoggerFactory logger, ICoreService githubActionsService)
{
private readonly ILogger _logger = logger.CreateLogger<LinkIndexLinkChecker>();

public async Task<int> CheckAll(ICoreService githubActionsService, Cancel ctx)
private sealed record RepositoryFilter
{
public string? LinksTo { get; set; }
public string? LinksFrom { get; set; }

public static RepositoryFilter None => new();
}

public async Task<int> CheckAll(Cancel ctx)
{
var fetcher = new LinksIndexCrossLinkFetcher(logger);
var resolver = new CrossLinkResolver(fetcher);
//todo add ctx
var crossLinks = await resolver.FetchLinks();

return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, null, ctx);
return await ValidateCrossLinks(crossLinks, resolver, RepositoryFilter.None, ctx);
}

public async Task<int> CheckRepository(ICoreService githubActionsService, string repository, Cancel ctx)
public async Task<int> CheckRepository(string? toRepository, string? fromRepository, Cancel ctx)
{
var fetcher = new LinksIndexCrossLinkFetcher(logger);
var resolver = new CrossLinkResolver(fetcher);
//todo add ctx
var crossLinks = await resolver.FetchLinks();
var filter = new RepositoryFilter
{
LinksTo = toRepository,
LinksFrom = fromRepository
};

return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, repository, ctx);
return await ValidateCrossLinks(crossLinks, resolver, filter, ctx);
}

public async Task<int> CheckWithLocalLinksJson(
ICoreService githubActionsService,
string repository,
string localLinksJson,
Cancel ctx
Expand Down Expand Up @@ -70,51 +82,64 @@ Cancel ctx
}

_logger.LogInformation("Validating all cross links to {Repository}:// from all repositories published to link-index.json", repository);
var filter = new RepositoryFilter
{
LinksTo = repository
};

return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, repository, ctx);
return await ValidateCrossLinks(crossLinks, resolver, filter, ctx);
}

private async Task<int> ValidateCrossLinks(
ICoreService githubActionsService,
FetchedCrossLinks crossLinks,
CrossLinkResolver resolver,
string? currentRepository,
RepositoryFilter filter,
Cancel ctx)
{
var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService);
_ = collector.StartAsync(ctx);
foreach (var (repository, linkReference) in crossLinks.LinkReferences)
{
if (!string.IsNullOrEmpty(currentRepository))
_logger.LogInformation("Validating '{CurrentRepository}://' links in {TargetRepository}", currentRepository, repository);
if (!string.IsNullOrEmpty(filter.LinksTo))
_logger.LogInformation("Validating '{CurrentRepository}://' links in {TargetRepository}", filter.LinksTo, repository);
else if (!string.IsNullOrEmpty(filter.LinksFrom))
{
if (repository != filter.LinksFrom)
continue;
_logger.LogInformation("Validating cross_links from {TargetRepository}", filter.LinksFrom);
}
else
_logger.LogInformation("Validating all cross_links in {Repository}", repository);

foreach (var crossLink in linkReference.CrossLinks)
{
// if we are filtering we only want errors from inbound links to a certain
// repository
var uri = new Uri(crossLink);
if (currentRepository != null && uri.Scheme != currentRepository)
if (filter.LinksTo != null && uri.Scheme != filter.LinksTo)
continue;

var linksJson = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{uri.Scheme}/main/links.json";
_ = resolver.TryResolve(s =>
{
if (s.Contains("is not a valid link in the"))
{
//
var error = $"'elastic/{repository}' links to unknown file: " + s;
error = error.Replace("is not a valid link in the", "in the");
collector.EmitError($"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{uri.Scheme}/main/links.json", error);
collector.EmitError(linksJson, error);
return;
}

collector.EmitError(repository, s);
}, uri, out _);
}, s => collector.EmitWarning(linksJson, s), uri, out _);
}
}

collector.Channel.TryComplete();
await collector.StopAsync(ctx);
return collector.Errors + collector.Warnings;
// non-strict for now
return collector.Errors;
// return collector.Errors + collector.Warnings;
}
}
7 changes: 4 additions & 3 deletions tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ public Task<FetchedCrossLinks> FetchLinks()
_crossLinks = new FetchedCrossLinks
{
DeclaredRepositories = DeclaredRepositories,
LinkReferences = LinkReferences.ToFrozenDictionary()
LinkReferences = LinkReferences.ToFrozenDictionary(),
FromConfiguration = true
};
return Task.FromResult(_crossLinks);
}

public bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
CrossLinkResolver.TryResolve(errorEmitter, _crossLinks, crossLinkUri, out resolvedUri);
public bool TryResolve(Action<string> errorEmitter, Action<string> warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, _crossLinks, crossLinkUri, out resolvedUri);
}
10 changes: 6 additions & 4 deletions tests/authoring/Framework/TestCrossLinkResolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,18 @@ type TestCrossLinkResolver (config: ConfigurationFile) =
let crossLinks =
FetchedCrossLinks(
DeclaredRepositories=this.DeclaredRepositories,
LinkReferences=this.LinkReferences.ToFrozenDictionary()
LinkReferences=this.LinkReferences.ToFrozenDictionary(),
FromConfiguration=true
)
Task.FromResult crossLinks

member this.TryResolve(errorEmitter, crossLinkUri, [<Out>]resolvedUri : byref<Uri|null>) =
member this.TryResolve(errorEmitter, warningEmitter, crossLinkUri, [<Out>]resolvedUri : byref<Uri|null>) =
let crossLinks =
FetchedCrossLinks(
DeclaredRepositories=this.DeclaredRepositories,
LinkReferences=this.LinkReferences.ToFrozenDictionary()
LinkReferences=this.LinkReferences.ToFrozenDictionary(),
FromConfiguration=true
)
CrossLinkResolver.TryResolve(errorEmitter, crossLinks, crossLinkUri, &resolvedUri);
CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, crossLinks, crossLinkUri, &resolvedUri);