From 709b2473967dc5ce26b3a87497ceff86fe95e1b4 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 18 May 2026 13:08:20 +0200 Subject: [PATCH 1/2] Collapse Gateway+Usecase layering into *Service per feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the ports-and-adapters boilerplate that required three types (interface + gateway impl + usecase orchestrator) for each feature. Each feature now has a single interface and implementation: - IFullSearchService / FullSearchService (was IGateway + Gateway + Usecase) - INavigationSearchService / NavigationSearchService - IChangesService / ChangesService - IOtlpService / AdotOtlpService IAskAiService/IAskAiMessageFeedbackService renamed from Gateway variants; AddElasticDocsApiUsecases → AddElasticDocsApiServices throughout. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../AskAi/AgentBuilderAskAiGateway.cs | 2 +- .../Adapters/AskAi/AskAiGatewayFactory.cs | 6 +- ...lasticsearchAskAiMessageFeedbackGateway.cs | 2 +- .../Adapters/AskAi/LlmGatewayAskAiGateway.cs | 2 +- .../AskAi/AskAiMessageFeedbackUsecase.cs | 43 ------- .../AskAi/AskAiUsecase.cs | 57 --------- .../AskAi/IAskAiGateway.cs | 4 +- .../AskAi/IAskAiMessageFeedbackGateway.cs | 4 +- .../Changes/ChangesUsecase.cs | 116 ------------------ .../MappingsExtensions.cs | 86 ++++++++++--- src/api/Elastic.Documentation.Api/Program.cs | 2 +- .../Search/FullSearchUsecase.cs | 97 --------------- .../Search/NavigationSearchUsecase.cs | 73 ----------- .../SerializationContext.cs | 12 +- .../ServicesExtension.cs | 66 ++++------ .../AdotOtlpService.cs} | 13 +- .../{IOtlpGateway.cs => IOtlpService.cs} | 4 +- .../Telemetry/OtlpProxyUsecase.cs | 36 ------ .../Tools/CoherenceTools.cs | 8 +- .../Tools/SearchTools.cs | 4 +- .../{ChangesGateway.cs => ChangesService.cs} | 99 ++++++++++++++- ...lSearchGateway.cs => FullSearchService.cs} | 42 ++++++- ...{IChangesGateway.cs => IChangesService.cs} | 22 +++- ...SearchGateway.cs => IFullSearchService.cs} | 24 +++- ...Gateway.cs => INavigationSearchService.cs} | 35 ++++-- ...kSearchGateway.cs => MockSearchService.cs} | 42 ++++--- ...hGateway.cs => NavigationSearchService.cs} | 34 ++++- .../ServicesExtension.cs | 8 +- .../docs-builder/Http/DocumentationWebHost.cs | 2 +- .../docs-builder/Http/StaticWebHost.cs | 2 +- .../Search/SearchIntegrationTests.cs | 14 +-- .../EuidEnrichmentIntegrationTests.cs | 9 +- .../OtlpProxyIntegrationTests.cs | 10 +- .../McpToolsIntegrationTestsBase.cs | 6 +- .../SearchRelevanceTests.cs | 4 +- 35 files changed, 406 insertions(+), 584 deletions(-) delete mode 100644 src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs delete mode 100644 src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs delete mode 100644 src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs delete mode 100644 src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs delete mode 100644 src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs rename src/api/Elastic.Documentation.Api/{Adapters/Telemetry/AdotOtlpGateway.cs => Telemetry/AdotOtlpService.cs} (87%) rename src/api/Elastic.Documentation.Api/Telemetry/{IOtlpGateway.cs => IOtlpService.cs} (90%) delete mode 100644 src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs rename src/services/Elastic.Documentation.Search/{ChangesGateway.cs => ChangesService.cs} (60%) rename src/services/Elastic.Documentation.Search/{FullSearchGateway.cs => FullSearchService.cs} (91%) rename src/services/Elastic.Documentation.Search/{IChangesGateway.cs => IChangesService.cs} (68%) rename src/services/Elastic.Documentation.Search/{IFullSearchGateway.cs => IFullSearchService.cs} (79%) rename src/services/Elastic.Documentation.Search/{INavigationSearchGateway.cs => INavigationSearchService.cs} (50%) rename src/services/Elastic.Documentation.Search/{MockSearchGateway.cs => MockSearchService.cs} (73%) rename src/services/Elastic.Documentation.Search/{NavigationSearchGateway.cs => NavigationSearchService.cs} (87%) diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs index 90b6e420c3..aba730b558 100644 --- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs @@ -12,7 +12,7 @@ namespace Elastic.Documentation.Api.Adapters.AskAi; -public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger logger) : IAskAiGateway +public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger logger) : IAskAiService { /// /// Model name used by Agent Builder (from AgentId) diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs index 26d280eaa9..4414370c3e 100644 --- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs @@ -9,18 +9,18 @@ namespace Elastic.Documentation.Api.Adapters.AskAi; /// -/// Factory that creates the appropriate IAskAiGateway based on the resolved provider +/// Factory that creates the appropriate IAskAiService based on the resolved provider /// public class AskAiGatewayFactory( IServiceProvider serviceProvider, AskAiProviderResolver providerResolver, - ILogger logger) : IAskAiGateway + ILogger logger) : IAskAiService { public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default) { var provider = providerResolver.ResolveProvider(); - IAskAiGateway gateway = provider switch + IAskAiService gateway = provider switch { "LlmGateway" => serviceProvider.GetRequiredService(), "AgentBuilder" => serviceProvider.GetRequiredService(), diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs index f7b898aa9d..d01d751b4e 100644 --- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs @@ -16,7 +16,7 @@ namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Records Ask AI message feedback to Elasticsearch. /// -public sealed class ElasticsearchAskAiMessageFeedbackGateway : IAskAiMessageFeedbackGateway, IDisposable +public sealed class ElasticsearchAskAiMessageFeedbackGateway : IAskAiMessageFeedbackService, IDisposable { private readonly ElasticsearchClient _client; private readonly string _indexName; diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs index 8807a79f24..fa8c90213e 100644 --- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs @@ -10,7 +10,7 @@ namespace Elastic.Documentation.Api.Adapters.AskAi; -public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiGateway +public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiService { /// /// Model name used by LLM Gateway (from PlatformContext.UseCase) diff --git a/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs deleted file mode 100644 index 7f08de6ab1..0000000000 --- a/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.AskAi; - -/// -/// Use case for handling Ask AI message feedback submissions. -/// -public class AskAiMessageFeedbackUsecase( - IAskAiMessageFeedbackGateway feedbackGateway, - ILogger logger) -{ - private static readonly ActivitySource FeedbackActivitySource = new(TelemetryConstants.AskAiFeedbackSourceName); - - public async Task SubmitFeedback(AskAiMessageFeedbackRequest request, string? euid, CancellationToken ctx) - { - using var activity = FeedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal); - _ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId); // correlation with chat traces - _ = activity?.SetTag("ask_ai.message.id", request.MessageId); - _ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant()); - // Note: user.euid is automatically added to spans by EuidSpanProcessor - - // MessageId and ConversationId are Guid types, so no sanitization needed - logger.LogInformation( - "Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}", - request.MessageId, - request.ConversationId, - request.Reaction); - - var record = new AskAiMessageFeedbackRecord( - request.MessageId, - request.ConversationId, - request.Reaction, - euid - ); - - await feedbackGateway.RecordFeedbackAsync(record, ctx); - } -} diff --git a/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs deleted file mode 100644 index 96efb4a9d1..0000000000 --- a/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.AskAi; - -public class AskAiUsecase( - IAskAiGateway askAiGateway, - IStreamTransformer streamTransformer, - ILogger logger) -{ - private static readonly ActivitySource AskAiActivitySource = new(TelemetryConstants.AskAiSourceName); - - public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx) - { - logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId); - var activity = AskAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client); - _ = activity?.SetTag("gen_ai.operation.name", "chat"); - _ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); // agent-builder or llm-gateway - _ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); // docs-agent or docs_assistant - if (askAiRequest.ConversationId is not null) - _ = activity?.SetTag("gen_ai.conversation.id", askAiRequest.ConversationId.ToString()); - - var inputMessages = new[] - { - new InputMessage("user", [new MessagePart("text", askAiRequest.Message)]) - }; - var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray); - _ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson); - var sanitizedMessage = askAiRequest.Message?.Replace("\r", "").Replace("\n", ""); - logger.LogInformation("AskAI input message: <{ask_ai.input.message}>", sanitizedMessage); - logger.LogInformation("Streaming AskAI response"); - - // Gateway handles conversation ID generation if needed - var response = await askAiGateway.AskAi(askAiRequest, ctx); - - // Use generated ID if available, otherwise use the original request ID - var conversationId = response.GeneratedConversationId ?? askAiRequest.ConversationId; - if (conversationId is not null) - _ = activity?.SetTag("gen_ai.conversation.id", conversationId.ToString()); - - // The stream transformer takes ownership of the activity and disposes it when streaming completes. - // This is necessary because streaming happens asynchronously after this method returns. - var transformedStream = await streamTransformer.TransformAsync( - response.Stream, - response.GeneratedConversationId, - activity, - ctx); - return transformedStream; - } -} - -public record AskAiRequest(string Message, Guid? ConversationId); diff --git a/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs index d1bb5c20da..4ab3e1f2f8 100644 --- a/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs @@ -15,7 +15,9 @@ namespace Elastic.Documentation.Api.AskAi; /// public record AskAiGatewayResponse(Stream Stream, Guid? GeneratedConversationId); -public interface IAskAiGateway +public record AskAiRequest(string Message, Guid? ConversationId); + +public interface IAskAiService { Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default); } diff --git a/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs index ab815decd5..9f3ca07407 100644 --- a/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs @@ -5,10 +5,10 @@ namespace Elastic.Documentation.Api.AskAi; /// -/// Gateway interface for recording Ask AI message feedback. +/// Service interface for recording Ask AI message feedback. /// Infrastructure implementations may use different storage backends (Elasticsearch, database, etc.) /// -public interface IAskAiMessageFeedbackGateway +public interface IAskAiMessageFeedbackService { /// /// Records feedback for a specific Ask AI message. diff --git a/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs b/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs deleted file mode 100644 index 797a7b66bb..0000000000 --- a/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Buffers; -using System.Text; -using System.Text.Json; -using Elastic.Documentation.Search; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Changes; - -/// Use case for the documentation changes feed. -public partial class ChangesUsecase(IChangesGateway changesGateway, ILogger logger) -{ - public async Task GetChangesAsync(ChangesApiRequest request, Cancel ctx = default) - { - var cursor = DecodeCursor(request.Cursor); - var pageSize = Math.Clamp(request.PageSize, 1, ChangesDefaults.MaxPageSize); - - var result = await changesGateway.GetChangesAsync( - new ChangesRequest - { - Since = request.Since, - PageSize = pageSize, - Cursor = cursor - }, - ctx - ); - - var nextCursor = result.NextCursor is not null - ? EncodeCursor(result.NextCursor) - : null; - - var hasMore = nextCursor is not null; - - LogChanges(logger, request.Since, result.Pages.Count, hasMore); - - return new ChangesApiResponse - { - Pages = result.Pages, - HasMore = hasMore, - NextCursor = nextCursor - }; - } - - private static ChangesPageCursor? DecodeCursor(string? cursor) - { - if (string.IsNullOrWhiteSpace(cursor)) - return null; - - try - { - var remainder = cursor.Length % 4; - var paddingLength = (4 - remainder) % 4; - var base64 = cursor - .Replace('-', '+') - .Replace('_', '/') - + new string('=', paddingLength); - - var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - var arrayLength = root.GetArrayLength(); - if (root.ValueKind != JsonValueKind.Array || arrayLength < 2) - return null; - - var epochEl = root[0]; - var urlEl = root[1]; - if (epochEl.ValueKind != JsonValueKind.Number || urlEl.ValueKind != JsonValueKind.String) - return null; - - return new ChangesPageCursor(epochEl.GetInt64(), urlEl.GetString()!); - } - catch (Exception ex) when (ex is FormatException or JsonException or InvalidOperationException) - { - return null; - } - } - - private static string EncodeCursor(ChangesPageCursor cursor) - { - var buffer = new ArrayBufferWriter(); - using var writer = new Utf8JsonWriter(buffer); - writer.WriteStartArray(); - writer.WriteNumberValue(cursor.ContentLastUpdatedEpochMs); - writer.WriteStringValue(cursor.Url); - writer.WriteEndArray(); - writer.Flush(); - - return Convert.ToBase64String(buffer.WrittenSpan) - .TrimEnd('=') - .Replace('+', '-') - .Replace('/', '_'); - } - - [LoggerMessage(Level = LogLevel.Information, - Message = "Changes feed returned {Count} pages since {Since} (hasMore: {HasMore})")] - private static partial void LogChanges(ILogger logger, DateTimeOffset since, int count, bool hasMore); -} - -/// API request for the changes feed endpoint. -public record ChangesApiRequest -{ - public required DateTimeOffset Since { get; init; } - public int PageSize { get; init; } = ChangesDefaults.PageSize; - public string? Cursor { get; init; } -} - -/// API response for the changes feed endpoint. -public record ChangesApiResponse -{ - public required IReadOnlyList Pages { get; init; } - public required bool HasMore { get; init; } - public string? NextCursor { get; init; } -} diff --git a/src/api/Elastic.Documentation.Api/MappingsExtensions.cs b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs index 493b9a1d67..8491c755bb 100644 --- a/src/api/Elastic.Documentation.Api/MappingsExtensions.cs +++ b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs @@ -2,16 +2,16 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Diagnostics; +using System.Text.Json; using Elastic.Documentation.Api.AskAi; -using Elastic.Documentation.Api.Changes; -using Elastic.Documentation.Api.Search; using Elastic.Documentation.Api.Telemetry; -using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Search; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; namespace Elastic.Documentation.Api; @@ -33,23 +33,71 @@ public static void MapElasticDocsApiEndpoints(this IEndpointRouteBuilder group, private static void MapAskAiEndpoint(IEndpointRouteBuilder group) { var askAiGroup = group.MapGroup("/ask-ai"); - _ = askAiGroup.MapPost("/stream", async (HttpContext context, AskAiRequest askAiRequest, AskAiUsecase askAiUsecase, Cancel ctx) => + _ = askAiGroup.MapPost("/stream", async (HttpContext context, AskAiRequest askAiRequest, IAskAiService askAiService, IStreamTransformer streamTransformer, ILogger logger, Cancel ctx) => { context.Response.ContentType = "text/event-stream"; context.Response.Headers.CacheControl = "no-cache"; context.Response.Headers.Connection = "keep-alive"; - var stream = await askAiUsecase.AskAi(askAiRequest, ctx); - await stream.CopyToAsync(context.Response.Body, ctx); + var askAiActivitySource = new ActivitySource(TelemetryConstants.AskAiSourceName); + logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId); + var activity = askAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client); + _ = activity?.SetTag("gen_ai.operation.name", "chat"); + _ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); + _ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); + if (askAiRequest.ConversationId is not null) + _ = activity?.SetTag("gen_ai.conversation.id", askAiRequest.ConversationId.ToString()); + + var inputMessages = new[] + { + new InputMessage("user", [new MessagePart("text", askAiRequest.Message)]) + }; + var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray); + _ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson); + var sanitizedMessage = askAiRequest.Message?.Replace("\r", "").Replace("\n", ""); + logger.LogInformation("AskAI input message: <{ask_ai.input.message}>", sanitizedMessage); + logger.LogInformation("Streaming AskAI response"); + + var response = await askAiService.AskAi(askAiRequest, ctx); + + var conversationId = response.GeneratedConversationId ?? askAiRequest.ConversationId; + if (conversationId is not null) + _ = activity?.SetTag("gen_ai.conversation.id", conversationId.ToString()); + + var transformedStream = await streamTransformer.TransformAsync( + response.Stream, + response.GeneratedConversationId, + activity, + ctx); + await transformedStream.CopyToAsync(context.Response.Body, ctx); }); // UUID validation is automatic via Guid type deserialization (returns 400 if invalid) - _ = askAiGroup.MapPost("/message-feedback", async (HttpContext context, AskAiMessageFeedbackRequest request, AskAiMessageFeedbackUsecase feedbackUsecase, Cancel ctx) => + _ = askAiGroup.MapPost("/message-feedback", async (HttpContext context, AskAiMessageFeedbackRequest request, IAskAiMessageFeedbackService feedbackService, ILogger logger, Cancel ctx) => { // Extract euid cookie for user tracking _ = context.Request.Cookies.TryGetValue("euid", out var euid); - await feedbackUsecase.SubmitFeedback(request, euid, ctx); + var feedbackActivitySource = new ActivitySource(TelemetryConstants.AskAiFeedbackSourceName); + using var activity = feedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal); + _ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId); + _ = activity?.SetTag("ask_ai.message.id", request.MessageId); + _ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant()); + + logger.LogInformation( + "Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}", + request.MessageId, + request.ConversationId, + request.Reaction); + + var record = new AskAiMessageFeedbackRecord( + request.MessageId, + request.ConversationId, + request.Reaction, + euid + ); + + await feedbackService.RecordFeedbackAsync(record, ctx); return Results.NoContent(); }).DisableAntiforgery(); } @@ -62,17 +110,17 @@ private static void MapNavigationSearch(IEndpointRouteBuilder group) [FromQuery(Name = "q")] string query, [FromQuery(Name = "page")] int? pageNumber, [FromQuery(Name = "type")] string? typeFilter, - NavigationSearchUsecase navigationSearchUsecase, + INavigationSearchService navigationSearchService, Cancel ctx ) => { - var request = new NavigationSearchApiRequest + var request = new NavigationSearchRequest { Query = query, PageNumber = pageNumber ?? 1, TypeFilter = typeFilter }; - var response = await navigationSearchUsecase.NavigationSearchAsync(request, ctx); + var response = await navigationSearchService.NavigationSearchAsync(request, ctx); return Results.Ok(response); }); } @@ -91,11 +139,11 @@ private static void MapFullSearch(IEndpointRouteBuilder group) [FromQuery(Name = "product")] string[]? productFilter, [FromQuery(Name = "version")] string? versionFilter, [FromQuery(Name = "sort")] string? sortBy, - FullSearchUsecase searchUsecase, + IFullSearchService searchService, Cancel ctx ) => { - var request = new FullSearchApiRequest + var request = new FullSearchRequest { Query = query, PageNumber = pageNumber ?? 1, @@ -107,7 +155,7 @@ Cancel ctx VersionFilter = versionFilter, SortBy = sortBy ?? "relevance" }; - var response = await searchUsecase.SearchAsync(request, ctx); + var response = await searchService.SearchAsync(request, ctx); return Results.Ok(response); }); } @@ -118,17 +166,17 @@ private static void MapChanges(IEndpointRouteBuilder group) => [FromQuery(Name = "since")] DateTimeOffset since, [FromQuery(Name = "cursor")] string? cursor, [FromQuery(Name = "size")] int? pageSize, - ChangesUsecase changesUsecase, + IChangesService changesService, Cancel ctx ) => { - var request = new ChangesApiRequest + var request = new ChangesRequest { Since = since, PageSize = pageSize ?? ChangesDefaults.PageSize, Cursor = cursor }; - var response = await changesUsecase.GetChangesAsync(request, ctx); + var response = await changesService.GetChangesAsync(request, ctx); return Results.Ok(response); }); @@ -147,10 +195,10 @@ private static void MapOtlpSignalEndpoint( string path, OtlpSignalType signalType) => group.MapPost(path, - async (HttpContext context, OtlpProxyUsecase proxyUsecase, Cancel ctx) => + async (HttpContext context, IOtlpService otlpService, Cancel ctx) => { var contentType = context.Request.ContentType ?? "application/json"; - var result = await proxyUsecase.ProxyOtlp( + var result = await otlpService.ForwardOtlp( signalType, context.Request.Body, contentType, diff --git a/src/api/Elastic.Documentation.Api/Program.cs b/src/api/Elastic.Documentation.Api/Program.cs index 98dec28313..bbee8a9201 100644 --- a/src/api/Elastic.Documentation.Api/Program.cs +++ b/src/api/Elastic.Documentation.Api/Program.cs @@ -31,7 +31,7 @@ var environment = Environment.GetEnvironmentVariable("ENVIRONMENT"); Console.WriteLine($"Docs Environment: {environment}"); - builder.Services.AddElasticDocsApiUsecases(environment); + builder.Services.AddElasticDocsApiServices(environment); var app = builder.Build(); var logger = app.Services.GetRequiredService>(); diff --git a/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs b/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs deleted file mode 100644 index 3eb6e84586..0000000000 --- a/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using Elastic.Documentation.Search; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Search; - -/// -/// Use case for full-page search operations. -/// -public partial class FullSearchUsecase(IFullSearchGateway fullSearchGateway, ILogger logger) -{ - public async Task SearchAsync(FullSearchApiRequest request, Cancel ctx = default) - { - var result = await fullSearchGateway.SearchAsync( - new FullSearchRequest - { - Query = request.Query, - PageNumber = request.PageNumber, - PageSize = request.PageSize, - TypeFilter = request.TypeFilter, - SectionFilter = request.SectionFilter, - DeploymentFilter = request.DeploymentFilter, - ProductFilter = request.ProductFilter, - VersionFilter = request.VersionFilter, - SortBy = request.SortBy - }, - ctx - ); - - var response = new FullSearchApiResponse - { - Results = result.Results, - TotalResults = result.TotalHits, - PageNumber = request.PageNumber, - PageSize = request.PageSize, - Aggregations = result.Aggregations, - IsSemanticQuery = result.IsSemanticQuery - }; - - LogFullSearchResults( - logger, - response.PageSize, - response.PageNumber, - request.Query, - result.IsSemanticQuery, - new FullSearchResultsLogProperties(result.Results.Select(i => i.Url).ToArray()) - ); - - return response; - } - - [LoggerMessage(Level = LogLevel.Information, Message = "Full search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}' (semantic: {IsSemantic})")] - private static partial void LogFullSearchResults( - ILogger logger, - int pageSize, - int pageNumber, - string searchQuery, - bool isSemantic, - [LogProperties] FullSearchResultsLogProperties result); - - private sealed record FullSearchResultsLogProperties(string[] Urls); -} - -/// -/// API request model for full-page search. -/// -public record FullSearchApiRequest -{ - public required string Query { get; init; } - public int PageNumber { get; init; } = 1; - public int PageSize { get; init; } = 20; - public string[]? TypeFilter { get; init; } - public string[]? SectionFilter { get; init; } - public string[]? DeploymentFilter { get; init; } - public string[]? ProductFilter { get; init; } - public string? VersionFilter { get; init; } - public string SortBy { get; init; } = "relevance"; -} - -/// -/// API response model for full-page search. -/// -public record FullSearchApiResponse -{ - public required IEnumerable Results { get; init; } - public required int TotalResults { get; init; } - public required int PageNumber { get; init; } - public required int PageSize { get; init; } - public FullSearchAggregations Aggregations { get; init; } = new(); - public bool IsSemanticQuery { get; init; } - public int PageCount => TotalResults > 0 - ? (int)Math.Ceiling((double)TotalResults / PageSize) - : 0; -} diff --git a/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs b/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs deleted file mode 100644 index 751a8182a3..0000000000 --- a/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using Elastic.Documentation.Search; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Search; - -// note still called SearchUseCase because we'll re-add Search() and ensure both share the same client. -public partial class NavigationSearchUsecase(INavigationSearchGateway navigationSearchGateway, ILogger logger) -{ - public async Task NavigationSearchAsync(NavigationSearchApiRequest request, Cancel ctx = default) - { - var result = await navigationSearchGateway.NavigationSearchAsync( - request.Query, - request.PageNumber, - request.PageSize, - request.TypeFilter, - ctx - ); - - var response = new NavigationSearchApiResponse - { - Results = result.Results, - TotalResults = result.TotalHits, - PageNumber = request.PageNumber, - PageSize = request.PageSize, - Aggregations = new NavigationSearchAggregations { Type = result.Aggregations } - }; - - LogNavigationSearchResults( - logger, - response.PageSize, - response.PageNumber, - request.Query, - new AutoCompleteResultsLogProperties(result.Results.Select(i => i.Url).ToArray()) - ); - - return response; - } - - [LoggerMessage(Level = LogLevel.Information, Message = "Navigation search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}'")] - private static partial void LogNavigationSearchResults(ILogger logger, int pageSize, int pageNumber, string searchQuery, [LogProperties] AutoCompleteResultsLogProperties result); - - private sealed record AutoCompleteResultsLogProperties(string[] Urls); -} - -public record NavigationSearchApiRequest -{ - public required string Query { get; init; } - public int PageNumber { get; init; } = 1; - public int PageSize { get; init; } = 20; - public string? TypeFilter { get; init; } -} - -public record NavigationSearchApiResponse -{ - public required IEnumerable Results { get; init; } - public required int TotalResults { get; init; } - public required int PageNumber { get; init; } - public required int PageSize { get; init; } - public NavigationSearchAggregations Aggregations { get; init; } = new(); - public int PageCount => TotalResults > 0 - ? (int)Math.Ceiling((double)TotalResults / PageSize) - : 0; -} - -public record NavigationSearchAggregations -{ - public IReadOnlyDictionary Type { get; init; } = new Dictionary(); -} - diff --git a/src/api/Elastic.Documentation.Api/SerializationContext.cs b/src/api/Elastic.Documentation.Api/SerializationContext.cs index b293ee9975..d5474683b9 100644 --- a/src/api/Elastic.Documentation.Api/SerializationContext.cs +++ b/src/api/Elastic.Documentation.Api/SerializationContext.cs @@ -4,8 +4,6 @@ using System.Text.Json.Serialization; using Elastic.Documentation.Api.AskAi; -using Elastic.Documentation.Api.Changes; -using Elastic.Documentation.Api.Search; using Elastic.Documentation.Search; namespace Elastic.Documentation.Api; @@ -22,19 +20,19 @@ public record OutputMessage(string Role, MessagePart[] Parts, string FinishReaso [JsonSerializable(typeof(AskAiRequest))] [JsonSerializable(typeof(AskAiMessageFeedbackRequest))] [JsonSerializable(typeof(Reaction))] -[JsonSerializable(typeof(NavigationSearchApiRequest))] -[JsonSerializable(typeof(NavigationSearchApiResponse))] +[JsonSerializable(typeof(NavigationSearchRequest))] +[JsonSerializable(typeof(NavigationSearchResponse))] [JsonSerializable(typeof(NavigationSearchAggregations))] [JsonSerializable(typeof(InputMessage))] [JsonSerializable(typeof(OutputMessage[]))] [JsonSerializable(typeof(MessagePart))] [JsonSerializable(typeof(InputMessage[]))] -[JsonSerializable(typeof(FullSearchApiRequest))] -[JsonSerializable(typeof(FullSearchApiResponse))] +[JsonSerializable(typeof(FullSearchRequest))] +[JsonSerializable(typeof(FullSearchResponse))] [JsonSerializable(typeof(FullSearchAggregations))] -[JsonSerializable(typeof(ChangesApiResponse))] +[JsonSerializable(typeof(ChangesResponse))] [JsonSerializable(typeof(ChangedPageDto))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] public partial class ApiJsonContext : JsonSerializerContext; diff --git a/src/api/Elastic.Documentation.Api/ServicesExtension.cs b/src/api/Elastic.Documentation.Api/ServicesExtension.cs index 520df0e6b9..4e846b57d7 100644 --- a/src/api/Elastic.Documentation.Api/ServicesExtension.cs +++ b/src/api/Elastic.Documentation.Api/ServicesExtension.cs @@ -6,12 +6,9 @@ using Amazon.DynamoDBv2; using Elastic.Documentation.Api; using Elastic.Documentation.Api.Adapters.AskAi; -using Elastic.Documentation.Api.Adapters.Telemetry; using Elastic.Documentation.Api.AskAi; using Elastic.Documentation.Api.Caching; -using Elastic.Documentation.Api.Changes; using Elastic.Documentation.Api.Gcp; -using Elastic.Documentation.Api.Search; using Elastic.Documentation.Api.Telemetry; using Elastic.Documentation.Search; using Microsoft.Extensions.Configuration; @@ -44,20 +41,20 @@ public static class ServicesExtension return loggerFactory?.CreateLogger(typeof(ServicesExtension)); } - public static void AddElasticDocsApiUsecases(this IServiceCollection services, string? appEnvironment) + public static void AddElasticDocsApiServices(this IServiceCollection services, string? appEnvironment) { if (AppEnvExtensions.TryParse(appEnvironment, out var parsedEnvironment, true)) - AddElasticDocsApiUsecases(services, parsedEnvironment); + AddElasticDocsApiServices(services, parsedEnvironment); else { var logger = GetLogger(services); logger?.LogWarning("Unable to parse environment {AppEnvironment} into AppEnvironment. Using default AppEnvironment.Dev", appEnvironment); - AddElasticDocsApiUsecases(services, AppEnv.Dev); + AddElasticDocsApiServices(services, AppEnv.Dev); } } - private static void AddElasticDocsApiUsecases(this IServiceCollection services, AppEnv appEnv) + private static void AddElasticDocsApiServices(this IServiceCollection services, AppEnv appEnv) { _ = services.ConfigureHttpJsonOptions(options => { @@ -74,9 +71,9 @@ private static void AddElasticDocsApiUsecases(this IServiceCollection services, // Register AppEnvironment as a singleton for dependency injection _ = services.AddSingleton(new AppEnvironment { Current = appEnv }); AddDistributedCache(services, appEnv); - AddAskAiUsecase(services, appEnv); - AddSearchUsecase(services, appEnv); - AddOtlpProxyUsecase(services, appEnv); + AddAskAiServices(services, appEnv); + AddSearchServices(services, appEnv); + AddOtlpProxyService(services, appEnv); } // Note: IParameterProvider is no longer needed - all options now read from IConfiguration (env vars) @@ -134,10 +131,10 @@ private static void AddDistributedCache(IServiceCollection services, AppEnv appE } } - private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv) + private static void AddAskAiServices(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); - logger?.LogInformation("Configuring AskAi use case for environment {AppEnvironment}", appEnv); + logger?.LogInformation("Configuring AskAi services for environment {AppEnvironment}", appEnv); try { @@ -153,9 +150,6 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv) _ = services.AddSingleton(); logger?.LogInformation("KibanaOptions registered successfully"); - _ = services.AddScoped(); - logger?.LogInformation("AskAiUsecase registered successfully"); - // Register HttpContextAccessor for provider resolution _ = services.AddHttpContextAccessor(); logger?.LogInformation("HttpContextAccessor registered successfully"); @@ -164,51 +158,46 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv) _ = services.AddScoped(); logger?.LogInformation("AskAiProviderResolver registered successfully"); - // Register both gateways as concrete types + // Register both service implementations as concrete types _ = services.AddScoped(); _ = services.AddScoped(); - logger?.LogInformation("Both AI gateways registered as concrete types"); + logger?.LogInformation("Both AI service implementations registered as concrete types"); // Register both transformers as concrete types _ = services.AddScoped(); _ = services.AddScoped(); logger?.LogInformation("Both stream transformers registered as concrete types"); - // Register factories as interface implementations - _ = services.AddScoped(); + // Register factory as interface implementation + _ = services.AddScoped(); _ = services.AddScoped(); - logger?.LogInformation("Gateway and transformer factories registered successfully - provider switchable via X-AI-Provider header"); + logger?.LogInformation("Service and transformer factories registered successfully - provider switchable via X-AI-Provider header"); - // Register message feedback components (gateway is singleton for connection reuse) - _ = services.AddSingleton(); - _ = services.AddScoped(); - logger?.LogInformation("AskAiMessageFeedbackUsecase and Elasticsearch gateway registered successfully"); + // Register message feedback service (singleton for connection reuse) + _ = services.AddSingleton(); + logger?.LogInformation("AskAiMessageFeedbackService (Elasticsearch) registered successfully"); } catch (Exception ex) { - logger?.LogError(ex, "Failed to configure AskAi use case for environment {AppEnvironment}", appEnv); + logger?.LogError(ex, "Failed to configure AskAi services for environment {AppEnvironment}", appEnv); throw; } } - private static void AddSearchUsecase(IServiceCollection services, AppEnv appEnv) + + private static void AddSearchServices(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); - logger?.LogInformation("Configuring Search use case for environment {AppEnvironment}", appEnv); + logger?.LogInformation("Configuring Search services for environment {AppEnvironment}", appEnv); - // Use the shared search service for DI registration (registers gateways) + // Use the shared search service for DI registration _ = services.AddSearchServices(); - - // Register usecases (depend on the gateways from search service) - _ = services.AddScoped(); - _ = services.AddScoped(); - logger?.LogInformation("Full search use case registered with hybrid RRF support"); - _ = services.AddScoped(); + logger?.LogInformation("Full search service registered with hybrid RRF support"); } - private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appEnv) + private static void AddOtlpProxyService(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); - logger?.LogInformation("Configuring OTLP proxy use case for environment {AppEnvironment}", appEnv); + logger?.LogInformation("Configuring OTLP proxy service for environment {AppEnvironment}", appEnv); _ = services.AddSingleton(sp => { @@ -217,14 +206,13 @@ private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appE }); // Register named HttpClient for OTLP proxy - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(30); }); - _ = services.AddScoped(); - _ = services.AddScoped(); + _ = services.AddScoped(); logger?.LogInformation("OTLP proxy configured to forward to ADOT Lambda Layer collector"); } } diff --git a/src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs similarity index 87% rename from src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs rename to src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs index 7f9414fc0a..0eede2c9a3 100644 --- a/src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs @@ -2,21 +2,22 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Diagnostics; using System.Net.Sockets; -using Elastic.Documentation.Api.Telemetry; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Adapters.Telemetry; +namespace Elastic.Documentation.Api.Telemetry; /// -/// Gateway that forwards OTLP telemetry to the ADOT Lambda Layer collector. +/// Service that forwards OTLP telemetry to the ADOT Lambda Layer collector. /// -public class AdotOtlpGateway( +public class AdotOtlpService( IHttpClientFactory httpClientFactory, OtlpProxyOptions options, - ILogger logger) : IOtlpGateway + ILogger logger) : IOtlpService { public const string HttpClientName = "OtlpProxy"; + private static readonly ActivitySource ActivitySource = new(TelemetryConstants.OtlpProxySourceName); private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); /// @@ -26,6 +27,8 @@ public async Task ForwardOtlp( string contentType, Cancel ctx = default) { + using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client); + try { var targetUrl = $"{options.Endpoint.TrimEnd('/')}/v1/{signalType.ToStringFast(true)}"; diff --git a/src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs similarity index 90% rename from src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs rename to src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs index 5c00f37ca2..77fcb9c19e 100644 --- a/src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs @@ -5,9 +5,9 @@ namespace Elastic.Documentation.Api.Telemetry; /// -/// Gateway for forwarding OTLP telemetry to a collector. +/// Service for forwarding OTLP telemetry to a collector. /// -public interface IOtlpGateway +public interface IOtlpService { /// /// Forwards OTLP telemetry data to the collector. diff --git a/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs deleted file mode 100644 index 5ac9963547..0000000000 --- a/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics; - -namespace Elastic.Documentation.Api.Telemetry; - -/// -/// Proxies OTLP telemetry from the frontend to the local ADOT Lambda Layer collector. -/// The ADOT layer handles authentication and forwarding to the backend. -/// -public class OtlpProxyUsecase(IOtlpGateway gateway) -{ - private static readonly ActivitySource ActivitySource = new(TelemetryConstants.OtlpProxySourceName); - - /// - /// Proxies OTLP data from the frontend to the local ADOT collector. - /// - /// The OTLP signal type (traces, logs, or metrics) - /// The raw OTLP payload (JSON or protobuf) - /// Content-Type header from the original request - /// Cancellation token - /// Result containing HTTP status code and response content - public async Task ProxyOtlp( - OtlpSignalType signalType, - Stream requestBody, - string contentType, - Cancel ctx = default) - { - using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client); - - // Forward to gateway - return await gateway.ForwardOtlp(signalType, requestBody, contentType, ctx); - } -} diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs index fd61e92110..49a6ede81b 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs @@ -18,7 +18,7 @@ namespace Elastic.Documentation.Mcp.Remote.Tools; /// MCP tools for checking documentation coherence and finding inconsistencies. /// [McpServerToolType] -public class CoherenceTools(IFullSearchGateway fullSearchGateway, ILogger logger) +public class CoherenceTools(IFullSearchService fullSearchGateway, ILogger logger) { /// /// Checks documentation coherence for a given topic. @@ -73,13 +73,13 @@ public async Task CheckCoherence( var response = new CoherenceCheckResponse { Topic = topic, - TotalDocuments = result.TotalHits, + TotalDocuments = result.TotalResults, AnalyzedDocuments = result.Results.Count, SectionCoverage = navigationSections, ProductCoverage = products, DocsWithAiSummary = docsWithSummaries, DocsWithRagSummary = docsWithRagSummaries, - CoverageScore = CalculateCoverageScore(result.TotalHits, navigationSections.Count, products.Count), + CoverageScore = CalculateCoverageScore(result.TotalResults, navigationSections.Count, products.Count), TopDocuments = result.Results.Take(5).Select(r => new CoherenceDocDto { Url = r.Url, @@ -203,7 +203,7 @@ public async Task FindInconsistencies( { Topic = topic, FocusArea = focusArea, - TotalDocuments = result.TotalHits, + TotalDocuments = result.TotalResults, PotentialInconsistencies = potentialOverlaps.Take(10).ToList(), ProductBreakdown = byProduct.ToDictionary(g => g.Key, g => g.Value.Count) }; diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs index 94778a98a2..021c245a20 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs @@ -18,7 +18,7 @@ namespace Elastic.Documentation.Mcp.Remote.Tools; /// MCP tools for semantic search operations on Elastic documentation. /// [McpServerToolType] -public class SearchTools(IFullSearchGateway fullSearchGateway, ILogger logger) +public class SearchTools(IFullSearchService fullSearchGateway, ILogger logger) { /// /// Performs semantic search across all Elastic documentation. @@ -70,7 +70,7 @@ public async Task SemanticSearch( var response = new SemanticSearchResponse { Query = query, - TotalHits = result.TotalHits, + TotalHits = result.TotalResults, IsSemanticQuery = result.IsSemanticQuery, Results = result.Results.Select(r => new SearchResultDto { diff --git a/src/services/Elastic.Documentation.Search/ChangesGateway.cs b/src/services/Elastic.Documentation.Search/ChangesService.cs similarity index 60% rename from src/services/Elastic.Documentation.Search/ChangesGateway.cs rename to src/services/Elastic.Documentation.Search/ChangesService.cs index 1e065a64dd..14e915ecb1 100644 --- a/src/services/Elastic.Documentation.Search/ChangesGateway.cs +++ b/src/services/Elastic.Documentation.Search/ChangesService.cs @@ -2,6 +2,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Buffers; +using System.Text; +using System.Text.Json; using Elastic.Clients.Elasticsearch; using Elastic.Documentation.Search.Common; using Microsoft.Extensions.Logging; @@ -9,17 +12,47 @@ namespace Elastic.Documentation.Search; /// -/// Elasticsearch gateway for the documentation changes feed. +/// Elasticsearch service for the documentation changes feed. /// Queries content_last_updated > since with search_after cursor pagination. /// Uses a shared Point In Time (PIT) for consistent pagination across requests. /// -public partial class ChangesGateway( +public partial class ChangesService( ElasticsearchClientAccessor clientAccessor, SharedPointInTimeManager pitManager, - ILogger logger -) : IChangesGateway + ILogger logger +) : IChangesService { - public async Task GetChangesAsync(ChangesRequest request, Cancel ctx = default) + public async Task GetChangesAsync(ChangesRequest request, Cancel ctx = default) + { + var cursor = DecodeCursor(request.Cursor); + var pageSize = Math.Clamp(request.PageSize, 1, ChangesDefaults.MaxPageSize); + + var internalRequest = new ChangesInternalRequest + { + Since = request.Since, + PageSize = pageSize, + Cursor = cursor + }; + + var result = await GetChangesInternalAsync(internalRequest, ctx); + + var nextCursor = result.NextCursor is not null + ? EncodeCursor(result.NextCursor) + : null; + + var hasMore = nextCursor is not null; + + LogChanges(logger, request.Since, result.Pages.Count, hasMore); + + return new ChangesResponse + { + Pages = result.Pages, + HasMore = hasMore, + NextCursor = nextCursor + }; + } + + private async Task GetChangesInternalAsync(ChangesInternalRequest request, Cancel ctx = default) { var fetchSize = request.PageSize + 1; @@ -56,7 +89,7 @@ public async Task GetChangesAsync(ChangesRequest request, Cancel } private async Task> Search( - ChangesRequest request, string pitId, int fetchSize, Cancel ctx + ChangesInternalRequest request, string pitId, int fetchSize, Cancel ctx ) => await clientAccessor.Client.SearchAsync(s => { @@ -146,6 +179,60 @@ private static ChangesResult BuildResult(SearchResponse r }; } + private static ChangesPageCursor? DecodeCursor(string? cursor) + { + if (string.IsNullOrWhiteSpace(cursor)) + return null; + + try + { + var remainder = cursor.Length % 4; + var paddingLength = (4 - remainder) % 4; + var base64 = cursor + .Replace('-', '+') + .Replace('_', '/') + + new string('=', paddingLength); + + var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var arrayLength = root.GetArrayLength(); + if (root.ValueKind != JsonValueKind.Array || arrayLength < 2) + return null; + + var epochEl = root[0]; + var urlEl = root[1]; + if (epochEl.ValueKind != JsonValueKind.Number || urlEl.ValueKind != JsonValueKind.String) + return null; + + return new ChangesPageCursor(epochEl.GetInt64(), urlEl.GetString()!); + } + catch (Exception ex) when (ex is FormatException or JsonException or InvalidOperationException) + { + return null; + } + } + + private static string EncodeCursor(ChangesPageCursor cursor) + { + var buffer = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(buffer); + writer.WriteStartArray(); + writer.WriteNumberValue(cursor.ContentLastUpdatedEpochMs); + writer.WriteStringValue(cursor.Url); + writer.WriteEndArray(); + writer.Flush(); + + return Convert.ToBase64String(buffer.WrittenSpan) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + + [LoggerMessage(Level = LogLevel.Information, + Message = "Changes feed returned {Count} pages since {Since} (hasMore: {HasMore})")] + private static partial void LogChanges(ILogger logger, DateTimeOffset since, int count, bool hasMore); + [LoggerMessage(Level = LogLevel.Warning, Message = "PIT expired or not found, opening a new one and retrying with existing search_after position")] private static partial void LogPitExpired(ILogger logger); } diff --git a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs b/src/services/Elastic.Documentation.Search/FullSearchService.cs similarity index 91% rename from src/services/Elastic.Documentation.Search/FullSearchGateway.cs rename to src/services/Elastic.Documentation.Search/FullSearchService.cs index 5eb7bd8931..7d78e4ec1e 100644 --- a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs +++ b/src/services/Elastic.Documentation.Search/FullSearchService.cs @@ -13,14 +13,14 @@ namespace Elastic.Documentation.Search; /// -/// Full-page search gateway implementation. +/// Full-page search service implementation. /// Uses hybrid RRF search for semantic queries, lexical-only for keyword queries. /// -public partial class FullSearchGateway( +public partial class FullSearchService( ElasticsearchClientAccessor clientAccessor, ProductsConfiguration productsConfiguration, - ILogger logger) - : IFullSearchGateway, IDisposable + ILogger logger) + : IFullSearchService, IDisposable { /// /// Regex pattern to detect semantic/question queries. @@ -60,17 +60,47 @@ private static bool IsSemanticQuery(string query) wordCount > 3; } - public async Task SearchAsync(FullSearchRequest request, Cancel ctx = default) + public async Task SearchAsync(FullSearchRequest request, Cancel ctx = default) { var isSemantic = IsSemanticQuery(request.Query); logger.LogDebug("Full search for query '{Query}' - semantic: {IsSemantic}", request.Query, isSemantic); - return isSemantic + var result = isSemantic ? await SearchWithHybridRrf(request, ctx) : await SearchLexicalOnly(request, ctx); + + var response = new FullSearchResponse + { + Results = result.Results, + TotalResults = result.TotalHits, + PageNumber = request.PageNumber, + PageSize = request.PageSize, + Aggregations = result.Aggregations, + IsSemanticQuery = result.IsSemanticQuery + }; + + LogFullSearchResults( + logger, + response.PageSize, + response.PageNumber, + request.Query, + result.IsSemanticQuery, + result.Results.Select(i => i.Url).ToArray() + ); + + return response; } + [LoggerMessage(Level = LogLevel.Information, Message = "Full search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}' (semantic: {IsSemantic}): {Urls}")] + private static partial void LogFullSearchResults( + ILogger logger, + int pageSize, + int pageNumber, + string searchQuery, + bool isSemantic, + string[] urls); + /// /// Performs hybrid RRF search combining lexical and semantic queries. /// Used when the query is detected as a semantic/question query. diff --git a/src/services/Elastic.Documentation.Search/IChangesGateway.cs b/src/services/Elastic.Documentation.Search/IChangesService.cs similarity index 68% rename from src/services/Elastic.Documentation.Search/IChangesGateway.cs rename to src/services/Elastic.Documentation.Search/IChangesService.cs index 63127c7b2d..73c67703a0 100644 --- a/src/services/Elastic.Documentation.Search/IChangesGateway.cs +++ b/src/services/Elastic.Documentation.Search/IChangesService.cs @@ -5,13 +5,29 @@ namespace Elastic.Documentation.Search; /// Gateway interface for querying documentation page changes. -public interface IChangesGateway +public interface IChangesService { - Task GetChangesAsync(ChangesRequest request, Cancel ctx = default); + Task GetChangesAsync(ChangesRequest request, Cancel ctx = default); } -/// Internal request for the changes gateway. +/// API request for the changes feed endpoint. public record ChangesRequest +{ + public required DateTimeOffset Since { get; init; } + public int PageSize { get; init; } = ChangesDefaults.PageSize; + public string? Cursor { get; init; } +} + +/// API response for the changes feed endpoint. +public record ChangesResponse +{ + public required IReadOnlyList Pages { get; init; } + public required bool HasMore { get; init; } + public string? NextCursor { get; init; } +} + +/// Internal request for the changes gateway. +public record ChangesInternalRequest { public required DateTimeOffset Since { get; init; } public int PageSize { get; init; } = ChangesDefaults.PageSize; diff --git a/src/services/Elastic.Documentation.Search/IFullSearchGateway.cs b/src/services/Elastic.Documentation.Search/IFullSearchService.cs similarity index 79% rename from src/services/Elastic.Documentation.Search/IFullSearchGateway.cs rename to src/services/Elastic.Documentation.Search/IFullSearchService.cs index 876ca4aee7..59af09bd69 100644 --- a/src/services/Elastic.Documentation.Search/IFullSearchGateway.cs +++ b/src/services/Elastic.Documentation.Search/IFullSearchService.cs @@ -5,12 +5,12 @@ namespace Elastic.Documentation.Search; /// -/// Gateway interface for full-page search operations. +/// Service interface for full-page search operations. /// Supports hybrid RRF search with semantic query detection. /// -public interface IFullSearchGateway +public interface IFullSearchService { - Task SearchAsync(FullSearchRequest request, Cancel ctx = default); + Task SearchAsync(FullSearchRequest request, Cancel ctx = default); } /// @@ -31,7 +31,23 @@ public record FullSearchRequest } /// -/// Result model for full-page search. +/// Response model for full-page search (unified API+service response). +/// +public record FullSearchResponse +{ + public required IReadOnlyList Results { get; init; } + public required int TotalResults { get; init; } + public required int PageNumber { get; init; } + public required int PageSize { get; init; } + public FullSearchAggregations Aggregations { get; init; } = new(); + public bool IsSemanticQuery { get; init; } + public int PageCount => TotalResults > 0 + ? (int)Math.Ceiling((double)TotalResults / PageSize) + : 0; +} + +/// +/// Result model for full-page search (used internally by the service implementation). /// public record FullSearchResult { diff --git a/src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs b/src/services/Elastic.Documentation.Search/INavigationSearchService.cs similarity index 50% rename from src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs rename to src/services/Elastic.Documentation.Search/INavigationSearchService.cs index 8e172d1834..9fdcf6feb3 100644 --- a/src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs +++ b/src/services/Elastic.Documentation.Search/INavigationSearchService.cs @@ -4,15 +4,34 @@ namespace Elastic.Documentation.Search; -public interface INavigationSearchGateway +public interface INavigationSearchService { - Task NavigationSearchAsync( - string query, - int pageNumber, - int pageSize, - string? filter = null, - Cancel ctx = default - ); + Task NavigationSearchAsync(NavigationSearchRequest request, Cancel ctx = default); +} + +public record NavigationSearchRequest +{ + public required string Query { get; init; } + public int PageNumber { get; init; } = 1; + public int PageSize { get; init; } = 20; + public string? TypeFilter { get; init; } +} + +public record NavigationSearchResponse +{ + public required IEnumerable Results { get; init; } + public required int TotalResults { get; init; } + public required int PageNumber { get; init; } + public required int PageSize { get; init; } + public NavigationSearchAggregations Aggregations { get; init; } = new(); + public int PageCount => TotalResults > 0 + ? (int)Math.Ceiling((double)TotalResults / PageSize) + : 0; +} + +public record NavigationSearchAggregations +{ + public IReadOnlyDictionary Type { get; init; } = new Dictionary(); } public record NavigationSearchResult diff --git a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs b/src/services/Elastic.Documentation.Search/MockSearchService.cs similarity index 73% rename from src/services/Elastic.Documentation.Search/MockSearchGateway.cs rename to src/services/Elastic.Documentation.Search/MockSearchService.cs index 74ced72ee9..a3dffb9672 100644 --- a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs +++ b/src/services/Elastic.Documentation.Search/MockSearchService.cs @@ -4,7 +4,7 @@ namespace Elastic.Documentation.Search; -public class MockNavigationSearchGateway : INavigationSearchGateway +public class MockNavigationSearchService : INavigationSearchService { private static readonly List Results = [ @@ -80,39 +80,53 @@ public class MockNavigationSearchGateway : INavigationSearchGateway } ]; - public async Task NavigationSearchAsync(string query, int pageNumber, int pageSize, string? filter = null, CancellationToken ctx = default) + public async Task NavigationSearchAsync(NavigationSearchRequest request, CancellationToken ctx = default) { var filteredResults = Results .Where(item => - item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || - item.Description?.Contains(query, StringComparison.OrdinalIgnoreCase) == true) + item.Title.Contains(request.Query, StringComparison.OrdinalIgnoreCase) || + item.Description?.Contains(request.Query, StringComparison.OrdinalIgnoreCase) == true) .ToList(); // Apply type filter if specified - if (!string.IsNullOrWhiteSpace(filter)) - filteredResults = filteredResults.Where(item => item.Type == filter).ToList(); + if (!string.IsNullOrWhiteSpace(request.TypeFilter)) + filteredResults = filteredResults.Where(item => item.Type == request.TypeFilter).ToList(); // Calculate aggregations before filtering var aggregations = Results .Where(item => - item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || - item.Description?.Contains(query, StringComparison.OrdinalIgnoreCase) == true) + item.Title.Contains(request.Query, StringComparison.OrdinalIgnoreCase) || + item.Description?.Contains(request.Query, StringComparison.OrdinalIgnoreCase) == true) .GroupBy(item => item.Type) .ToDictionary(g => g.Key, g => (long)g.Count()); var pagedResults = filteredResults - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize) + .Skip((request.PageNumber - 1) * request.PageSize) + .Take(request.PageSize) .ToList(); - Console.WriteLine($"MockSearchGateway: Paged results count: {pagedResults.Count}"); + Console.WriteLine($"MockSearchService: Paged results count: {pagedResults.Count}"); await Task.Delay(1000, ctx); - return new NavigationSearchResult + return new NavigationSearchResponse { - TotalHits = filteredResults.Count, + TotalResults = filteredResults.Count, Results = pagedResults, - Aggregations = aggregations + PageNumber = request.PageNumber, + PageSize = request.PageSize, + Aggregations = new NavigationSearchAggregations { Type = aggregations } }; } } + +public class MockFullSearchService : IFullSearchService +{ + public Task SearchAsync(FullSearchRequest request, Cancel ctx = default) => + Task.FromResult(new FullSearchResponse + { + Results = [], + TotalResults = 0, + PageNumber = request.PageNumber, + PageSize = request.PageSize + }); +} diff --git a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs b/src/services/Elastic.Documentation.Search/NavigationSearchService.cs similarity index 87% rename from src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs rename to src/services/Elastic.Documentation.Search/NavigationSearchService.cs index 67f3814c0c..49d3267a21 100644 --- a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs +++ b/src/services/Elastic.Documentation.Search/NavigationSearchService.cs @@ -12,16 +12,40 @@ namespace Elastic.Documentation.Search; /// -/// Elasticsearch gateway for Navigation Search (autocomplete/navigation search). +/// Elasticsearch service for Navigation Search (autocomplete/navigation search). /// Uses shared lexical query optimized for autocomplete. /// -public class NavigationSearchGateway(ElasticsearchClientAccessor clientAccessor, ILogger logger) - : INavigationSearchGateway, IDisposable +public partial class NavigationSearchService(ElasticsearchClientAccessor clientAccessor, ILogger logger) + : INavigationSearchService, IDisposable { public async Task CanConnect(Cancel ctx) => await clientAccessor.CanConnect(ctx); - public async Task NavigationSearchAsync(string query, int pageNumber, int pageSize, string? filter = null, Cancel ctx = default) => - await SearchImplementation(query, pageNumber, pageSize, filter, ctx); + public async Task NavigationSearchAsync(NavigationSearchRequest request, Cancel ctx = default) + { + var result = await SearchImplementation(request.Query, request.PageNumber, request.PageSize, request.TypeFilter, ctx); + + var response = new NavigationSearchResponse + { + Results = result.Results, + TotalResults = result.TotalHits, + PageNumber = request.PageNumber, + PageSize = request.PageSize, + Aggregations = new NavigationSearchAggregations { Type = result.Aggregations } + }; + + LogNavigationSearchResults( + logger, + response.PageSize, + response.PageNumber, + request.Query, + result.Results.Select(i => i.Url).ToArray() + ); + + return response; + } + + [LoggerMessage(Level = LogLevel.Information, Message = "Navigation search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}': {Urls}")] + private static partial void LogNavigationSearchResults(ILogger logger, int pageSize, int pageNumber, string searchQuery, string[] urls); public async Task SearchImplementation(string query, int pageNumber, int pageSize, string? filter = null, Cancel ctx = default) { diff --git a/src/services/Elastic.Documentation.Search/ServicesExtension.cs b/src/services/Elastic.Documentation.Search/ServicesExtension.cs index 373b5a1c3a..b6f92c4133 100644 --- a/src/services/Elastic.Documentation.Search/ServicesExtension.cs +++ b/src/services/Elastic.Documentation.Search/ServicesExtension.cs @@ -27,15 +27,15 @@ public static IServiceCollection AddSearchServices(this IServiceCollection servi _ = services.AddSingleton(); // Navigation Search (autocomplete/navigation search) - _ = services.AddScoped(); + _ = services.AddScoped(); // FullSearch (full-page search with hybrid RRF) - _ = services.AddScoped(); - logger?.LogInformation("Full search gateways registered with hybrid RRF support"); + _ = services.AddScoped(); + logger?.LogInformation("Full search services registered with hybrid RRF support"); // Changes feed (cursor-paginated changes since a given date) _ = services.AddSingleton(); - _ = services.AddScoped(); + _ = services.AddScoped(); return services; } diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs index cf89234236..adfb7ef3de 100644 --- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs +++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs @@ -53,7 +53,7 @@ bool isWatchBuild _ = builder.AddDocumentationServiceDefaults(); #if DEBUG - builder.Services.AddElasticDocsApiUsecases("dev"); + builder.Services.AddElasticDocsApiServices("dev"); #endif _ = builder.Logging diff --git a/src/tooling/docs-builder/Http/StaticWebHost.cs b/src/tooling/docs-builder/Http/StaticWebHost.cs index 539ed4e8ac..250d772184 100644 --- a/src/tooling/docs-builder/Http/StaticWebHost.cs +++ b/src/tooling/docs-builder/Http/StaticWebHost.cs @@ -39,7 +39,7 @@ public StaticWebHost(int port, string? path) _ = builder.AddDocumentationServiceDefaults(); #if DEBUG - builder.Services.AddElasticDocsApiUsecases("dev"); + builder.Services.AddElasticDocsApiServices("dev"); #endif _ = builder.Logging diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs index 4e3e212695..4f351d3927 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs @@ -4,7 +4,7 @@ using System.Net.Http.Json; using AwesomeAssertions; -using Elastic.Documentation.Api.Search; +using Elastic.Documentation.Search; namespace Elastic.Assembler.IntegrationTests.Search; @@ -55,18 +55,18 @@ public async Task SearchEndpointReturnsExpectedFirstResult(string query, string // Assert - Response should be successful response.EnsureSuccessStatusCode(); - var searchResponse = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + var searchResponse = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); searchResponse.Should().NotBeNull("Search response should be deserialized"); // Log results for debugging output.WriteLine($"Query: {query}"); output.WriteLine($"Total results: {searchResponse.TotalResults}"); - output.WriteLine($"Results returned: {searchResponse.Results.Count()}"); + output.WriteLine($"Results returned: {searchResponse.Results.Count}"); if (searchResponse.Results.Any()) { output.WriteLine("First result:"); - var firstResult = searchResponse.Results.First(); + var firstResult = searchResponse.Results[0]; output.WriteLine($" Title: {firstResult.Title}"); output.WriteLine($" URL: {firstResult.Url}"); output.WriteLine($" Score: {firstResult.Score}"); @@ -76,7 +76,7 @@ public async Task SearchEndpointReturnsExpectedFirstResult(string query, string searchResponse.Results.Should().NotBeEmpty($"Search for '{query}' should return results"); // Assert - First result should match expected URL - var actualFirstResultUrl = searchResponse.Results.First().Url; + var actualFirstResultUrl = searchResponse.Results[0].Url; actualFirstResultUrl.Should().Be(expectedFirstResultUrl, $"First result for query '{query}' should be the expected documentation page"); } @@ -93,12 +93,12 @@ public async Task SearchEndpointWithPaginationReturnsCorrectPage() // Act - Get first page var page1Response = await searchFixture.HttpClient.GetAsync($"/docs/_api/v1/search?q={Uri.EscapeDataString(query)}&page=1", TestContext.Current.CancellationToken); page1Response.EnsureSuccessStatusCode(); - var page1Data = await page1Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + var page1Data = await page1Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); // Act - Get second page var page2Response = await searchFixture.HttpClient.GetAsync($"/docs/_api/v1/search?q={Uri.EscapeDataString(query)}&page=2", TestContext.Current.CancellationToken); page2Response.EnsureSuccessStatusCode(); - var page2Data = await page2Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + var page2Data = await page2Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); // Assert page1Data.Should().NotBeNull(); diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs index c2a28743bc..c555951748 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs @@ -50,8 +50,8 @@ public async Task AskAiEndpointPropagatatesEuidToAllSpansAndLogs() // Create factory with mocked AskAi services using var factory = ApiWebApplicationFactory.WithMockedServices(services => { - // Mock IAskAiGateway to avoid external AI service calls - var mockAskAiGateway = A.Fake(); + // Mock IAskAiService to avoid external AI service calls + var mockAskAiGateway = A.Fake(); A.CallTo(() => mockAskAiGateway.AskAi(A._, A._)) .ReturnsLazily(() => { @@ -113,11 +113,10 @@ public async Task AskAiEndpointPropagatatesEuidToAllSpansAndLogs() var logRecords = factory.ExportedLogRecords; logRecords.Should().NotBeEmpty("Should have captured log records"); - // Find a log entry from AskAiUsecase + // Find a log entry from the AskAI endpoint handler var askAiLogRecord = logRecords.FirstOrDefault(r => - string.Equals(r.CategoryName, typeof(AskAiUsecase).FullName, StringComparison.OrdinalIgnoreCase) && r.FormattedMessage?.Contains("Starting AskAI", StringComparison.OrdinalIgnoreCase) == true); - askAiLogRecord.Should().NotBeNull("Should have logged from AskAiUsecase"); + askAiLogRecord.Should().NotBeNull("Should have logged from AskAI endpoint handler"); // Verify euid is present in OTEL log attributes (mirrors production exporter behavior) var euidAttribute = askAiLogRecord.Attributes?.FirstOrDefault(a => a.Key == TelemetryConstants.UserEuidAttributeName) ?? default; diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs index 714ba2b7ac..99db7df7da 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs @@ -5,8 +5,8 @@ using System.Net; using System.Text; using AwesomeAssertions; -using Elastic.Documentation.Api.Adapters.Telemetry; using Elastic.Documentation.Api.IntegrationTests.Fixtures; +using Elastic.Documentation.Api.Telemetry; using FakeItEasy; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -52,7 +52,7 @@ public async Task OtlpProxyTracesEndpointForwardsToCorrectUrl() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { // Replace the named HttpClient with our mock - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler); }); @@ -116,7 +116,7 @@ public async Task OtlpProxyLogsEndpointForwardsToCorrectUrl() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler); }); @@ -173,7 +173,7 @@ public async Task OtlpProxyMetricsEndpointForwardsToCorrectUrl() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler); }); @@ -225,7 +225,7 @@ public async Task OtlpProxyReturnsCollectorErrorStatusCode() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { #pragma warning disable EXTEXP0001 // Experimental API - needed for test to bypass resilience handlers - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler) .RemoveAllResilienceHandlers(); #pragma warning restore EXTEXP0001 diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs index 7dc58a73b8..7c00de5d58 100644 --- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs +++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs @@ -47,10 +47,10 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C var clientAccessor = CreateElasticsearchClientAccessor(); var productsConfig = CreateProductsConfiguration(); - var fullSearchGateway = new FullSearchGateway( + var fullSearchGateway = new FullSearchService( clientAccessor, productsConfig, - NullLogger.Instance + NullLogger.Instance ); var searchTools = new SearchTools(fullSearchGateway, NullLogger.Instance); @@ -77,7 +77,7 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C var clientAccessor = CreateElasticsearchClientAccessor(); var productsConfig = CreateProductsConfiguration(); - var fullSearchGateway = new FullSearchGateway(clientAccessor, productsConfig, NullLogger.Instance); + var fullSearchGateway = new FullSearchService(clientAccessor, productsConfig, NullLogger.Instance); var coherenceTools = new CoherenceTools(fullSearchGateway, NullLogger.Instance); return (coherenceTools, clientAccessor); } diff --git a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs index 4f25592bf8..060a75e7ff 100644 --- a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs +++ b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs @@ -239,14 +239,14 @@ public async Task ExplainTopResultAndExpectedAsyncReturnsDetailedScoring() /// /// Creates an ElasticsearchGateway instance using configuration from the distributed application. /// - private static (NavigationSearchGateway Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway() + private static (NavigationSearchService Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway() { var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler", environment: "dev"); var configProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new FileSystem(), configurationSource: ConfigurationSource.Embedded); var searchConfig = configProvider.CreateSearchConfiguration(); var clientAccessor = new ElasticsearchClientAccessor(endpoints, searchConfig); - var gateway = new NavigationSearchGateway(clientAccessor, NullLogger.Instance); + var gateway = new NavigationSearchService(clientAccessor, NullLogger.Instance); return (gateway, clientAccessor); } } From c4a011ef014e882fcf340e9dbec689b6cc694de3 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 19 May 2026 11:22:04 +0200 Subject: [PATCH 2/2] [3/5] VSA: Slim Elastic.Documentation, extract heavy deps to Tooling (#3345) Co-authored-by: Claude Sonnet 4.6 (1M context) --- docs-builder.slnx | 6 +- .../Elasticsearch/OpenApiDocumentExporter.cs | 3 +- .../Operations/AvailabilityBadgeHelper.cs | 1 + .../Building/CodexBuildService.cs | 1 + .../Assembler/AssemblyConfiguration.cs | 1 + .../BuildContext.cs | 2 +- .../Builder/ConfigurationFile.cs | 2 +- .../{ => Builder}/CrossLinkEntry.cs | 2 +- .../{ => Builder}/DocSetRegistry.cs | 2 +- .../Builder/RedirectFile.cs | 2 +- .../Changelog/ChangelogConfiguration.cs | 1 + .../ChangelogConfigurationLoader.cs | 7 +- .../Changelog}/ChangelogConfigurationYaml.cs | 26 +-- .../Changelog}/TypeEntryYamlConverter.cs | 2 +- .../Changelog}/YamlLenientList.cs | 4 +- .../Changelog}/YamlLenientListConverter.cs | 2 +- ...Elastic.Documentation.Configuration.csproj | 2 +- .../Inference/VersionInference.cs | 1 + .../ReleaseNotes/BundleLoader.cs | 1 + .../LenientStringListConverter.cs | 66 -------- .../Serialization/YamlStaticContext.cs | 15 ++ .../SystemEnvironmentVariables.cs | 2 + .../Toc/DocumentationSetFile.cs | 2 +- .../Versions/VersionConfiguration.cs | 1 + .../VersionsConfigurationExtensions.cs | 1 + .../CrossLinks/CrossLinkFetcher.cs | 1 + .../CrossLinks/CrossLinkResolver.cs | 1 + .../InboundLinks/LinkIndexLinkChecker.cs | 4 +- .../Elastic.Documentation.Tooling.csproj | 27 ++++ .../Exporter.cs | 5 +- .../ExternalCommandExecutor.cs | 0 .../ExternalCommands/NoopConsoleWriter.cs | 0 .../FileSystemFactory.cs | 2 + .../GitCheckoutInformationFactory.cs | 149 +++++++++++++++++ .../GlobalCliOptions.cs | 2 + .../Paths.cs | 2 + .../AppliesTo/Applicability.cs | 4 +- .../AppliesTo/ApplicabilitySelector.cs | 2 + .../AppliesTo/ApplicableTo.cs | 96 +++++++++-- .../AppliesTo/ApplicableToJsonConverter.cs | 11 +- .../AppliesTo/ProductApplicability.cs | 51 +++--- .../AppliesTo/ProductLifecycle.cs | 21 ++- .../Diagnostics/DiagnosticsCollector.cs | 7 +- .../Diagnostics/IDiagnosticsCollector.cs | 6 +- .../Elastic.Documentation.csproj | 15 +- .../GitCheckoutInformation.cs | 151 +----------------- .../IEnvironmentVariables.cs | 2 +- .../ChangelogEntrySubtype.cs | 2 +- .../{ => ReleaseNotes}/ChangelogEntryType.cs | 2 +- .../Serialization/SourceGenerationContext.cs | 2 + .../SymlinkValidator.cs | 2 +- .../{ => Versions}/SemVersion.cs | 3 +- .../{ => Versions}/VersionOrDate.cs | 2 +- .../{ => Versions}/VersionSpec.cs | 2 +- src/Elastic.Markdown/Elastic.Markdown.csproj | 1 + .../ElasticsearchMarkdownExporter.Export.cs | 2 +- .../AppliesTo/ApplicableToYamlConverter.cs | 3 + .../Myst/AppliesTo/SemVersionConverter.cs} | 3 + .../Myst/Components/ApplicabilityRenderer.cs | 1 + .../Myst/Components/ApplicableToViewModel.cs | 1 + .../Directives/Changelog/ChangelogBlock.cs | 1 + .../Changelog/ChangelogInlineRenderer.cs | 1 + .../Directives/CsvInclude/CsvIncludeBlock.cs | 1 + .../Myst/Directives/Include/IncludeBlock.cs | 1 + .../Myst/Directives/Settings/SettingsBlock.cs | 1 + .../Myst/Directives/Version/VersionBlock.cs | 1 + .../SubstitutionMutationHelper.cs | 1 + .../Myst/Roles/AppliesTo/AppliesToRole.cs | 1 + .../Myst/YamlSerialization.cs | 1 - .../Elastic.Documentation.Api.csproj | 2 +- .../Elastic.Documentation.Mcp.Remote.csproj | 2 +- .../McpBearerAuthMiddleware.cs | 1 + .../McpOAuthMetadata.cs | 1 + .../Bundling/ChangelogBundleAmendService.cs | 1 - .../Bundling/ChangelogBundlingService.cs | 1 - .../Bundling/ChangelogRemoveService.cs | 1 - .../Creation/ChangelogCreationService.cs | 2 +- .../ChangelogPrEvaluationService.cs | 1 - .../ChangelogPrepareArtifactService.cs | 2 +- .../GitHubReleaseChangelogService.cs | 1 - .../Asciidoc/ChangelogAsciidocRenderer.cs | 2 +- .../Rendering/ChangelogRenderingService.cs | 2 +- .../BreakingChangesMarkdownRenderer.cs | 2 +- .../Markdown/ChangelogGfmRenderer.cs | 2 +- .../Markdown/DeprecationsMarkdownRenderer.cs | 2 +- .../Markdown/IndexMarkdownRenderer.cs | 2 +- .../Markdown/KnownIssuesMarkdownRenderer.cs | 2 +- .../ChangelogYamlStaticContext.cs | 24 --- .../Uploading/ChangelogUploadService.cs | 2 +- .../Building/AssemblerBuildService.cs | 1 + .../Navigation/GlobalNavigationService.cs | 2 +- .../IsolatedBuildService.cs | 1 + .../AppliesToEntry.cs | 28 ++++ .../DocumentationDocument.cs | 3 +- .../DocumentationMappingConfig.cs | 0 ...astic.Documentation.Search.Contract.csproj | 17 ++ .../IndexedProduct.cs | 0 .../ChangesService.cs | 0 .../Common/ElasticsearchClientAccessor.cs | 0 .../Common/SearchQueryBuilder.cs | 0 .../Common/SearchResultProcessor.cs | 0 .../Elastic.Documentation.Search.csproj | 4 +- .../EsJsonContext.cs | 0 .../FullSearchService.cs | 0 .../IChangesService.cs | 0 .../IFullSearchService.cs | 0 .../INavigationSearchService.cs | 0 .../MockSearchService.cs | 0 .../NavigationSearchService.cs | 0 .../ServicesExtension.cs | 0 .../SharedPointInTimeManager.cs | 0 .../StringHighlightExtensions.cs | 0 .../docs-builder/Commands/ChangelogCommand.cs | 3 +- .../docs-builder/Http/DocumentationWebHost.cs | 3 +- .../Middleware/CheckForUpdatesMiddleware.cs | 1 + .../TestHelpers.cs | 1 + .../Mcp.Remote.IntegrationTests.csproj | 2 +- .../Search.IntegrationTests.csproj | 2 +- .../OpenApiDocumentExporterTests.cs | 1 + .../Elastic.ApiExplorer.Tests/TestHelpers.cs | 1 + .../Changelogs/ChangelogConfigurationTests.cs | 4 +- .../Changelogs/ChangelogTestBase.cs | 1 + .../Create/CreateChangelogTestBase.cs | 1 + .../Create/ReleaseNoteExtractionTests.cs | 1 + .../Changelogs/Render/ErrorHandlingTests.cs | 2 +- .../Creation/CIEnrichmentTests.cs | 1 + .../Creation/ChangelogCreationServiceTests.cs | 1 + ...umentation.Api.Infrastructure.Tests.csproj | 2 +- .../MockEnvironmentVariables.cs | 1 + .../SymlinkValidationTests.cs | 1 + .../TestHelpers.cs | 1 + .../ApiConfigurationTests.cs | 2 +- .../AssemblyConfigurationMatchTests.cs | 1 + .../ConfigurationFileExcludeTests.cs | 2 +- .../CrossLinkRegistryTests.cs | 2 +- .../DocumentInferrerServiceTests.cs | 1 + ...c.Documentation.Configuration.Tests.csproj | 1 + .../ReleaseNotes/VersionOrDateTests.cs | 1 + .../VersionInferenceTests.cs | 1 + ...ApplicableToJsonConverterRoundTripTests.cs | 1 + ...icableToJsonConverterSerializationTests.cs | 1 + .../ProductApplicabilityToStringTests.cs | 15 +- .../CrossLinks/UriEnvironmentResolverTests.cs | 1 + .../Directives/VersionTests.cs | 1 + .../DocSet/RepositoryLinksTests.cs | 2 +- .../Elastic.Markdown.Tests.csproj | 1 + ...DocumentationDocumentSerializationTests.cs | 41 ++--- tests/Elastic.Markdown.Tests/TestHelpers.cs | 1 + tests/authoring/Framework/Setup.fs | 1 + 149 files changed, 544 insertions(+), 422 deletions(-) rename src/Elastic.Documentation.Configuration/{ => Builder}/CrossLinkEntry.cs (91%) rename src/Elastic.Documentation.Configuration/{ => Builder}/DocSetRegistry.cs (93%) rename src/{services/Elastic.Changelog/Configuration => Elastic.Documentation.Configuration/Changelog}/ChangelogConfigurationLoader.cs (99%) rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/ChangelogConfigurationYaml.cs (95%) rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/TypeEntryYamlConverter.cs (98%) rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/YamlLenientList.cs (86%) rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/YamlLenientListConverter.cs (98%) delete mode 100644 src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs create mode 100644 src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/Exporter.cs (56%) rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/ExternalCommands/ExternalCommandExecutor.cs (100%) rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/ExternalCommands/NoopConsoleWriter.cs (100%) rename src/{Elastic.Documentation.Configuration => Elastic.Documentation.Tooling}/FileSystemFactory.cs (98%) create mode 100644 src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/GlobalCliOptions.cs (84%) rename src/{Elastic.Documentation.Configuration => Elastic.Documentation.Tooling}/Paths.cs (98%) rename src/{Elastic.Documentation.Configuration => Elastic.Documentation}/IEnvironmentVariables.cs (98%) rename src/Elastic.Documentation/{ => ReleaseNotes}/ChangelogEntrySubtype.cs (96%) rename src/Elastic.Documentation/{ => ReleaseNotes}/ChangelogEntryType.cs (97%) rename src/{Elastic.Documentation.Configuration => Elastic.Documentation}/SymlinkValidator.cs (98%) rename src/Elastic.Documentation/{ => Versions}/SemVersion.cs (99%) rename src/Elastic.Documentation/{ => Versions}/VersionOrDate.cs (99%) rename src/Elastic.Documentation/{ => Versions}/VersionSpec.cs (99%) rename src/{Elastic.Documentation => Elastic.Markdown/Myst}/AppliesTo/ApplicableToYamlConverter.cs (98%) rename src/{Elastic.Documentation/AppliesTo/AllVersions.cs => Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs} (87%) delete mode 100644 src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs create mode 100644 src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs rename src/{Elastic.Documentation/Search => services/search/Elastic.Documentation.Search.Contract}/DocumentationDocument.cs (98%) rename src/{Elastic.Documentation/Search => services/search/Elastic.Documentation.Search.Contract}/DocumentationMappingConfig.cs (100%) create mode 100644 src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj rename src/{Elastic.Documentation/Search => services/search/Elastic.Documentation.Search.Contract}/IndexedProduct.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/ChangesService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/Common/SearchResultProcessor.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj (74%) rename src/services/{ => search}/Elastic.Documentation.Search/EsJsonContext.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/FullSearchService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/IChangesService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/IFullSearchService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/INavigationSearchService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/MockSearchService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/NavigationSearchService.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/ServicesExtension.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/SharedPointInTimeManager.cs (100%) rename src/services/{ => search}/Elastic.Documentation.Search/StringHighlightExtensions.cs (100%) diff --git a/docs-builder.slnx b/docs-builder.slnx index f5a36e725b..12215d0721 100644 --- a/docs-builder.slnx +++ b/docs-builder.slnx @@ -53,6 +53,7 @@ + @@ -71,7 +72,10 @@ - + + + + diff --git a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs index 8c17cc0223..38a90d901e 100644 --- a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs +++ b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Configuration.Inference; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Search; +using Elastic.Documentation.Versions; using Microsoft.OpenApi; using Microsoft.OpenApi.Reader; @@ -177,7 +178,7 @@ private IEnumerable ConvertToDocuments(OpenApiDocument op StrippedBody = body, Headings = headings, Links = [], - Applies = applies, + Applies = applies?.ToAppliesTo(), Parents = [ new ParentDocument { Title = "API Reference", Url = "/docs/api" }, diff --git a/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs b/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs index bd1b0cb869..ba8cbf3cdd 100644 --- a/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs +++ b/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs @@ -8,6 +8,7 @@ using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Versions; using Microsoft.OpenApi; namespace Elastic.ApiExplorer.Operations; diff --git a/src/Elastic.Codex/Building/CodexBuildService.cs b/src/Elastic.Codex/Building/CodexBuildService.cs index 5d2efb8222..8a5ba281d0 100644 --- a/src/Elastic.Codex/Building/CodexBuildService.cs +++ b/src/Elastic.Codex/Building/CodexBuildService.cs @@ -10,6 +10,7 @@ using Elastic.Codex.Sourcing; using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Configuration.Codex; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Isolated; diff --git a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs index 133ab98db6..f9833a0772 100644 --- a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Extensions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs index 28cf823d1e..5ea7d914a9 100644 --- a/src/Elastic.Documentation.Configuration/BuildContext.cs +++ b/src/Elastic.Documentation.Configuration/BuildContext.cs @@ -117,7 +117,7 @@ public BuildContext( if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName) DocumentationSourceDirectory = ConfigurationPath.Directory!; - Git = gitCheckoutInformation ?? GitCheckoutInformation.Create(DocumentationCheckoutDirectory, ReadFileSystem); + Git = gitCheckoutInformation ?? GitCheckoutInformationFactory.Create(DocumentationCheckoutDirectory, ReadFileSystem); // Load and resolve the docset file, or create an empty one if it doesn't exist ConfigurationYaml = ConfigurationPath.Exists diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs index d3a0b8eec8..f20055f83b 100644 --- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs @@ -11,7 +11,7 @@ using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; using Elastic.Documentation.Links; -using static Elastic.Documentation.Configuration.SymlinkValidator; +using static Elastic.Documentation.SymlinkValidator; namespace Elastic.Documentation.Configuration.Builder; diff --git a/src/Elastic.Documentation.Configuration/CrossLinkEntry.cs b/src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs similarity index 91% rename from src/Elastic.Documentation.Configuration/CrossLinkEntry.cs rename to src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs index 9dc66af13d..88e2e591d4 100644 --- a/src/Elastic.Documentation.Configuration/CrossLinkEntry.cs +++ b/src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation.Configuration.Builder; /// /// A parsed cross-link entry from docset.yml, with the target registry for lookup. diff --git a/src/Elastic.Documentation.Configuration/DocSetRegistry.cs b/src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs similarity index 93% rename from src/Elastic.Documentation.Configuration/DocSetRegistry.cs rename to src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs index cfcf9bbf23..ae06ae372d 100644 --- a/src/Elastic.Documentation.Configuration/DocSetRegistry.cs +++ b/src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation.Configuration.Builder; /// /// Registry type for cross-link resolution. Maps to the link index source: diff --git a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs index ca43dadb84..ebb6a12597 100644 --- a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs @@ -5,7 +5,7 @@ using System.IO.Abstractions; using Elastic.Documentation.Links; using YamlDotNet.RepresentationModel; -using static Elastic.Documentation.Configuration.SymlinkValidator; +using static Elastic.Documentation.SymlinkValidator; namespace Elastic.Documentation.Configuration.Builder; diff --git a/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs index 6b52229a0a..f24c55019c 100644 --- a/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.ReleaseNotes; namespace Elastic.Documentation.Configuration.Changelog; diff --git a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs similarity index 99% rename from src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs rename to src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs index 4c866be716..38a73ee728 100644 --- a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs @@ -3,11 +3,10 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Changelog.Serialization; using Elastic.Documentation; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.Configuration.Serialization; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; using Microsoft.Extensions.Logging; @@ -15,7 +14,7 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace Elastic.Changelog.Configuration; +namespace Elastic.Documentation.Configuration.Changelog; /// /// Service for loading and validating changelog configuration @@ -25,7 +24,7 @@ public class ChangelogConfigurationLoader(ILoggerFactory logFactory, IConfigurat private readonly ILogger _logger = logFactory.CreateLogger(); private static readonly IDeserializer ConfigurationDeserializer = - new StaticDeserializerBuilder(new ChangelogYamlStaticContext()) + new StaticDeserializerBuilder(new YamlStaticContext()) .WithNamingConvention(UnderscoredNamingConvention.Instance) .WithTypeConverter(new YamlLenientListConverter()) .WithTypeConverter(new TypeEntryYamlConverter()) diff --git a/src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs similarity index 95% rename from src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs rename to src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs index 03c56e9b8e..80521f6742 100644 --- a/src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs @@ -2,13 +2,13 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// Internal DTO for YAML deserialization of changelog configuration. /// Maps directly to the YAML file structure. /// -internal record ChangelogConfigurationYaml +internal sealed record ChangelogConfigurationYaml { /// /// Filename strategy for generated changelog files (pr, issue, timestamp). @@ -57,7 +57,7 @@ internal record ChangelogConfigurationYaml /// /// Internal DTO for rules configuration in YAML. /// -internal record RulesConfigurationYaml +internal sealed record RulesConfigurationYaml { /// /// Global match mode for multi-valued fields ("any" or "all"). Default: "any". @@ -83,7 +83,7 @@ internal record RulesConfigurationYaml /// /// Internal DTO for bundle rules in YAML. /// -internal record BundleRulesYaml +internal sealed record BundleRulesYaml { /// /// Product IDs to exclude from the bundle (string or list). Cannot be combined with IncludeProducts. @@ -134,7 +134,7 @@ internal record BundleRulesYaml /// /// Internal DTO for create rules in YAML. /// -internal record CreateRulesYaml +internal sealed record CreateRulesYaml { /// /// Labels to exclude (string or list). Cannot be combined with Include. @@ -161,7 +161,7 @@ internal record CreateRulesYaml /// /// Internal DTO for publish rules in YAML. /// -internal record PublishRulesYaml +internal sealed record PublishRulesYaml { /// /// Match mode for areas ("any" or "all"). Inherits from rules.match if not set. @@ -198,7 +198,7 @@ internal record PublishRulesYaml /// /// Internal DTO for pivot configuration in YAML. /// -internal record PivotConfigurationYaml +internal sealed record PivotConfigurationYaml { /// /// Type definitions with optional labels and subtypes. @@ -231,7 +231,7 @@ internal record PivotConfigurationYaml /// /// Internal DTO for products configuration in YAML. /// -internal record ProductsConfigYaml +internal sealed record ProductsConfigYaml { /// /// List of available product IDs (string or list, empty = all from products.yml). @@ -247,7 +247,7 @@ internal record ProductsConfigYaml /// /// Internal DTO for default product specification in YAML. /// -internal record DefaultProductYaml +internal sealed record DefaultProductYaml { /// /// Product ID. @@ -263,7 +263,7 @@ internal record DefaultProductYaml /// /// Internal DTO for bundle configuration in YAML. /// -internal record BundleConfigurationYaml +internal sealed record BundleConfigurationYaml { /// /// Input directory containing changelog YAML files. @@ -314,7 +314,7 @@ internal record BundleConfigurationYaml /// /// Internal DTO for bundle profile in YAML. /// -internal record BundleProfileYaml +internal sealed record BundleProfileYaml { /// /// Product filter pattern for input changelogs. @@ -371,7 +371,7 @@ internal record BundleProfileYaml /// /// Internal DTO for extract configuration in YAML. /// -internal record ExtractConfigurationYaml +internal sealed record ExtractConfigurationYaml { /// /// Whether to extract release note text from PR or issue descriptions for the changelog entry description by default. @@ -397,7 +397,7 @@ internal record ExtractConfigurationYaml /// Internal DTO for type entry in YAML. /// Can represent either a simple label string or an object with labels and subtypes. /// -internal record TypeEntryYaml +internal sealed record TypeEntryYaml { /// /// Labels for this type (comma-separated string). diff --git a/src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs b/src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs similarity index 98% rename from src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs rename to src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs index 57c328b24f..135a5ffd78 100644 --- a/src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs @@ -6,7 +6,7 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// YAML type converter for TypeEntryYaml that handles both string and object forms. diff --git a/src/services/Elastic.Changelog/Serialization/YamlLenientList.cs b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs similarity index 86% rename from src/services/Elastic.Changelog/Serialization/YamlLenientList.cs rename to src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs index 724f8538fb..58c77ce908 100644 --- a/src/services/Elastic.Changelog/Serialization/YamlLenientList.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs @@ -2,14 +2,14 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// Wrapper type for YAML fields that can be specified as either a comma-separated string /// or a YAML list/sequence. Deserialized by . /// Uses mutable property for compatibility with YamlDotNet source generator. /// -internal class YamlLenientList +internal sealed class YamlLenientList { public List? Values { get; set; } diff --git a/src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs similarity index 98% rename from src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs rename to src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs index 49bbbb2d53..8facb13be3 100644 --- a/src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs @@ -6,7 +6,7 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// YAML type converter for that accepts both string and list forms. diff --git a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj index 8796dc5fd7..ceb479fab7 100644 --- a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj +++ b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj @@ -13,6 +13,7 @@ + @@ -20,7 +21,6 @@ - diff --git a/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs b/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs index 68f9b6ffd4..c7d176f59a 100644 --- a/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs +++ b/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs @@ -6,6 +6,7 @@ using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Configuration.Inference; diff --git a/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs b/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs index 7956d58190..9dabf2d9ed 100644 --- a/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs +++ b/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.Text.RegularExpressions; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Versions; using YamlDotNet.Core; namespace Elastic.Documentation.Configuration.ReleaseNotes; diff --git a/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs b/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs deleted file mode 100644 index 9b10e71934..0000000000 --- a/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Elastic.Documentation.Configuration.Serialization; - -/// -/// YAML type converter for List<string> that accepts both comma-separated strings and YAML sequences. -/// Used by the minimal changelog DTO deserializer so that publish blocker fields like types and areas -/// can be specified as either "deprecation, known-issue" or a YAML list. -/// -public class LenientStringListConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(List); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (parser.TryConsume(out var scalar)) - { - if (string.IsNullOrEmpty(scalar.Value) || scalar.Value == "~") - return null; - - var items = scalar.Value - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .ToList(); - - return items.Count > 0 ? items : null; - } - - if (parser.TryConsume(out _)) - { - var items = new List(); - - while (!parser.TryConsume(out _)) - { - var item = parser.Consume(); - if (!string.IsNullOrWhiteSpace(item.Value)) - items.Add(item.Value.Trim()); - } - - return items.Count > 0 ? items : null; - } - - return null; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) - { - if (value is not List { Count: > 0 } items) - { - emitter.Emit(new Scalar(null, null, string.Empty, ScalarStyle.Plain, true, false)); - return; - } - - emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block)); - - foreach (var item in items) - emitter.Emit(new Scalar(null, null, item, ScalarStyle.Plain, true, false)); - - emitter.Emit(new SequenceEnd()); - } -} diff --git a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs index 3bb5d61f93..21cab10863 100644 --- a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs +++ b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Configuration.Assembler; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.Codex; using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; @@ -61,4 +62,18 @@ namespace Elastic.Documentation.Configuration.Serialization; [YamlSerializable(typeof(ChangelogConfigMinimalDto))] [YamlSerializable(typeof(RulesConfigMinimalDto))] [YamlSerializable(typeof(PublishRulesMinimalDto))] +// Changelog YAML DTOs for CLI configuration (changelog.yml) +[YamlSerializable(typeof(ChangelogConfigurationYaml))] +[YamlSerializable(typeof(PivotConfigurationYaml))] +[YamlSerializable(typeof(TypeEntryYaml))] +[YamlSerializable(typeof(RulesConfigurationYaml))] +[YamlSerializable(typeof(CreateRulesYaml))] +[YamlSerializable(typeof(BundleRulesYaml))] +[YamlSerializable(typeof(PublishRulesYaml))] +[YamlSerializable(typeof(ProductsConfigYaml))] +[YamlSerializable(typeof(DefaultProductYaml))] +[YamlSerializable(typeof(BundleConfigurationYaml))] +[YamlSerializable(typeof(BundleProfileYaml))] +[YamlSerializable(typeof(ExtractConfigurationYaml))] +[YamlSerializable(typeof(YamlLenientList))] public partial class YamlStaticContext; diff --git a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs index b8e2d010d9..c9a7228981 100644 --- a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs +++ b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs @@ -2,6 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation; + namespace Elastic.Documentation.Configuration; /// diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs index 78e024151a..7e06c6c24d 100644 --- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs @@ -10,7 +10,7 @@ using Elastic.Documentation.Extensions; using Nullean.ScopedFileSystem; using YamlDotNet.Serialization; -using static Elastic.Documentation.Configuration.SymlinkValidator; +using static Elastic.Documentation.SymlinkValidator; namespace Elastic.Documentation.Configuration.Toc; diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs index f3ab2edf5d..48af422a4e 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.ComponentModel.DataAnnotations; +using Elastic.Documentation.Versions; using NetEscapades.EnumGenerators; using YamlDotNet.Serialization; diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs index 290b88c752..3458a686ea 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation.Versions; using YamlDotNet.Serialization; namespace Elastic.Documentation.Configuration.Versions; diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs index 2c54f1d374..d4dc8d5cca 100644 --- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs +++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs @@ -7,6 +7,7 @@ using System.IO.Abstractions; using System.Text.Json; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.LinkIndex; using Elastic.Documentation.Serialization; using Microsoft.Extensions.Logging; diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs index abaf584c2b..d329393b40 100644 --- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs +++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs @@ -5,6 +5,7 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; namespace Elastic.Documentation.Links.CrossLinks; diff --git a/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs b/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs index 8e65d8137f..fc1ae6e2aa 100644 --- a/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs +++ b/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs @@ -38,7 +38,7 @@ public async Task CheckRepository(IDiagnosticsCollector collector, string? var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); if (fromRepository == null && toRepository == null) { - fromRepository ??= GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName; + fromRepository ??= GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName; if (fromRepository == null) throw new Exception("Unable to determine repository name"); } @@ -58,7 +58,7 @@ public async Task CheckWithLocalLinksJson(IDiagnosticsCollector collector, { file ??= ".artifacts/docs/html/links.json"; var root = !string.IsNullOrEmpty(path) ? fileSystem.DirectoryInfo.New(path) : fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); - var repository = GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName + var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName ?? throw new Exception("Unable to determine repository name"); var localLinksJson = fileSystem.FileInfo.New(Path.Join(root.FullName, file)); diff --git a/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj b/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj new file mode 100644 index 0000000000..1ab221484e --- /dev/null +++ b/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + Elastic.Documentation + true + true + $(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574 + + + + + + + + + + + + + + + + + diff --git a/src/Elastic.Documentation/Exporter.cs b/src/Elastic.Documentation.Tooling/Exporter.cs similarity index 56% rename from src/Elastic.Documentation/Exporter.cs rename to src/Elastic.Documentation.Tooling/Exporter.cs index 25adbdf2df..6c45c60551 100644 --- a/src/Elastic.Documentation/Exporter.cs +++ b/src/Elastic.Documentation.Tooling/Exporter.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using Nullean.Argh; -using static Elastic.Documentation.Exporter; namespace Elastic.Documentation; @@ -21,6 +20,6 @@ public enum Exporter public static class ExportOptions { - public static HashSet Default { get; } = [Html, LLMText, Configuration, DocumentationState, LinkMetadata, Redirects]; - public static HashSet MetadataOnly { get; } = [Configuration, DocumentationState, LinkMetadata, Redirects]; + public static HashSet Default { get; } = [Exporter.Html, Exporter.LLMText, Exporter.Configuration, Exporter.DocumentationState, Exporter.LinkMetadata, Exporter.Redirects]; + public static HashSet MetadataOnly { get; } = [Exporter.Configuration, Exporter.DocumentationState, Exporter.LinkMetadata, Exporter.Redirects]; } diff --git a/src/Elastic.Documentation/ExternalCommands/ExternalCommandExecutor.cs b/src/Elastic.Documentation.Tooling/ExternalCommands/ExternalCommandExecutor.cs similarity index 100% rename from src/Elastic.Documentation/ExternalCommands/ExternalCommandExecutor.cs rename to src/Elastic.Documentation.Tooling/ExternalCommands/ExternalCommandExecutor.cs diff --git a/src/Elastic.Documentation/ExternalCommands/NoopConsoleWriter.cs b/src/Elastic.Documentation.Tooling/ExternalCommands/NoopConsoleWriter.cs similarity index 100% rename from src/Elastic.Documentation/ExternalCommands/NoopConsoleWriter.cs rename to src/Elastic.Documentation.Tooling/ExternalCommands/NoopConsoleWriter.cs diff --git a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/FileSystemFactory.cs rename to src/Elastic.Documentation.Tooling/FileSystemFactory.cs index 9f8b55b4cc..09cb7411ee 100644 --- a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs +++ b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs @@ -6,6 +6,8 @@ using System.IO.Abstractions.TestingHelpers; using Nullean.ScopedFileSystem; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.Configuration; public static class FileSystemFactory diff --git a/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs new file mode 100644 index 0000000000..db58b514b3 --- /dev/null +++ b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs @@ -0,0 +1,149 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions; +using System.Text.RegularExpressions; +using Elastic.Documentation.Extensions; +using Microsoft.Extensions.Logging; +using Nullean.ScopedFileSystem; +using SoftCircuits.IniFileParser; + +namespace Elastic.Documentation; + +public static partial class GitCheckoutInformationFactory +{ + // manual read because libgit2sharp is not yet AOT ready + public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem fileSystem, ILogger? logger = null) + { + if (source is null) + return GitCheckoutInformation.Unavailable; + + // Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType + // (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper + // rather than relying on the outer type name. + var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType(); + if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)) + { + return new GitCheckoutInformation + { + Branch = $"test-e35fcb27-5f60-4e", + Remote = "elastic/docs-builder", + Ref = "e35fcb27-5f60-4e", + RepositoryName = "docs-builder" + }; + } + var fakeRef = Guid.NewGuid().ToString()[..16]; + + var gitDir = GitDir(source, ".git"); + if (!gitDir.Exists) + { + var worktreeFile = Git(source, ".git"); + if (!worktreeFile.Exists) + return GitCheckoutInformation.Unavailable; + var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty); + if (workTreePath is null) + return GitCheckoutInformation.Unavailable; + gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git"); + if (gitDir is null || !gitDir.Exists) + return GitCheckoutInformation.Unavailable; + } + + var gitConfig = Git(gitDir, "config"); + if (!gitConfig.Exists) + { + logger?.LogInformation("Git checkout information not available."); + return GitCheckoutInformation.Unavailable; + } + + var head = Read(gitDir, "HEAD") ?? fakeRef; + var gitRef = head; + var branch = head.Replace("refs/heads/", string.Empty); + if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase)) + { + head = head.Replace("ref: ", string.Empty); + gitRef = Read(gitDir, head) ?? fakeRef; + branch = branch.Replace("ref: ", string.Empty); + } + else + branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head"; + + var ini = new IniFile(); + using var stream = gitConfig.OpenRead(); + using var streamReader = new StreamReader(stream); + ini.Load(streamReader); + + var remote = BranchTrackingRemote(branch, ini); + logger?.LogInformation("Remote from branch: {GitRemote}", remote); + if (string.IsNullOrEmpty(remote)) + { + remote = BranchTrackingRemote("main", ini); + logger?.LogInformation("Remote from main branch: {GitRemote}", remote); + } + + if (string.IsNullOrEmpty(remote)) + { + remote = BranchTrackingRemote("master", ini); + logger?.LogInformation("Remote from master branch: {GitRemote}", remote); + } + + if (string.IsNullOrEmpty(remote)) + { + remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); + logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote); + } + + if (string.IsNullOrEmpty(remote)) + { + remote = "elastic/docs-builder-unknown"; + logger?.LogInformation("Remote from fallback: {GitRemote}", remote); + } + remote = CutOffGitExtension().Replace(remote, string.Empty); + + var githubRef = Environment.GetEnvironmentVariable("GITHUB_REF"); + var info = new GitCheckoutInformation + { + Ref = gitRef, + Branch = branch, + Remote = remote, + RepositoryName = remote.Split('/').Last(), + GitHubRef = string.IsNullOrEmpty(githubRef) ? null : githubRef + }; + + logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote); + logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName); + return info; + + IFileInfo Git(IDirectoryInfo directoryInfo, string path) => + fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path)); + + IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) => + fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path)); + + string? Read(IDirectoryInfo directoryInfo, string path) + { + var gitPath = Git(directoryInfo, path).FullName; + return !fileSystem.File.Exists(gitPath) + ? null + : fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray()); + } + + string BranchTrackingRemote(string b, IniFile c) + { + var sections = c.GetSections(); + var branchSection = $"branch \"{b}\""; + if (!sections.Contains(branchSection)) + return string.Empty; + + var remoteName = ini.GetSetting(branchSection, "remote")?.Trim(); + + var remoteSection = $"remote \"{remoteName}\""; + + remote = ini.GetSetting(remoteSection, "url"); + return remote ?? string.Empty; + } + } + + [GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)] + private static partial Regex CutOffGitExtension(); +} diff --git a/src/Elastic.Documentation/GlobalCliOptions.cs b/src/Elastic.Documentation.Tooling/GlobalCliOptions.cs similarity index 84% rename from src/Elastic.Documentation/GlobalCliOptions.cs rename to src/Elastic.Documentation.Tooling/GlobalCliOptions.cs index cc0af96f8b..c6c203b8b8 100644 --- a/src/Elastic.Documentation/GlobalCliOptions.cs +++ b/src/Elastic.Documentation.Tooling/GlobalCliOptions.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging; +// ReSharper disable once CheckNamespace — intentionally preserving the namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation; /// diff --git a/src/Elastic.Documentation.Configuration/Paths.cs b/src/Elastic.Documentation.Tooling/Paths.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/Paths.cs rename to src/Elastic.Documentation.Tooling/Paths.cs index fa24bef977..a3d7011301 100644 --- a/src/Elastic.Documentation.Configuration/Paths.cs +++ b/src/Elastic.Documentation.Tooling/Paths.cs @@ -6,6 +6,8 @@ using System.IO.Abstractions; using Elastic.Documentation.Extensions; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.Configuration; public static class Paths diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index e65d174ba5..10eb1c4b74 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -6,11 +6,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Elastic.Documentation.Diagnostics; -using YamlDotNet.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.AppliesTo; -[YamlSerializable] public record AppliesCollection : IReadOnlyCollection { private readonly Applicability[] _items; @@ -169,7 +168,6 @@ public override string ToString() public int Count => _items.Length; } -[YamlSerializable] public record Applicability : IComparable, IComparable { public ProductLifecycle Lifecycle { get; init; } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index eb4a0f7f36..aa70d28259 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -2,6 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation.Versions; + namespace Elastic.Documentation.AppliesTo; /// diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs index 5bee5c81f2..c5a971b9cb 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs @@ -3,10 +3,12 @@ // See the LICENSE file in the project root for more information using System.Collections; +using System.Reflection; using System.Text; using System.Text.Json.Serialization; using Elastic.Documentation.Diagnostics; -using YamlDotNet.Serialization; +using Elastic.Documentation.Search; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.AppliesTo; @@ -34,26 +36,20 @@ public interface IApplicableToElement ApplicableTo? AppliesTo { get; } } -[YamlSerializable] [JsonConverter(typeof(ApplicableToJsonConverter))] public record ApplicableTo { - [YamlMember(Alias = "stack")] public AppliesCollection? Stack { get; set; } - [YamlMember(Alias = "deployment")] public DeploymentApplicability? Deployment { get; set; } - [YamlMember(Alias = "serverless")] public ServerlessProjectApplicability? Serverless { get; set; } - [YamlMember(Alias = "product")] public AppliesCollection? Product { get; set; } public ProductApplicability? ProductApplicability { get; set; } [JsonIgnore] - [YamlIgnore] public ApplicabilityDiagnosticsCollection? Diagnostics { get; set; } public static ApplicableTo All { get; } = new() @@ -72,6 +68,83 @@ public record ApplicableTo Serverless = ServerlessProjectApplicability.All }; + /// + /// Convert this rich applicability description into the flat wire-format entries indexed in Elasticsearch + /// under applies_to. Produces the same JSON shape as ApplicableToJsonConverter. + /// + public IReadOnlyCollection ToAppliesTo() + { + var entries = new List(); + + if (Stack is not null) + AddEntries(entries, "stack", "stack", Stack); + + if (Deployment is not null) + { + if (Deployment.Self is not null) + AddEntries(entries, "deployment", "self", Deployment.Self); + if (Deployment.Ece is not null) + AddEntries(entries, "deployment", "ece", Deployment.Ece); + if (Deployment.Eck is not null) + AddEntries(entries, "deployment", "eck", Deployment.Eck); + if (Deployment.Ess is not null) + AddEntries(entries, "deployment", "ess", Deployment.Ess); + } + + if (Serverless is not null) + { + if (Serverless.Elasticsearch is not null) + AddEntries(entries, "serverless", "elasticsearch", Serverless.Elasticsearch); + if (Serverless.Observability is not null) + AddEntries(entries, "serverless", "observability", Serverless.Observability); + if (Serverless.Security is not null) + AddEntries(entries, "serverless", "security", Serverless.Security); + } + + if (Product is not null) + AddEntries(entries, "product", "product", Product); + + if (ProductApplicability is not null) + { + foreach (var prop in typeof(ProductApplicability).GetProperties()) + { + var name = prop.GetCustomAttribute()?.Name; + if (name is null) + continue; + if (prop.GetValue(ProductApplicability) is AppliesCollection coll) + AddEntries(entries, "product", name, coll); + } + } + + return entries; + } + + private static void AddEntries(List sink, string type, string subType, AppliesCollection coll) + { + foreach (var a in coll) + sink.Add(new AppliesToEntry + { + Type = type, + SubType = subType, + Lifecycle = LifecycleName(a.Lifecycle), + Version = a.Version?.ToString() + }); + } + + private static string LifecycleName(ProductLifecycle lc) => lc switch + { + ProductLifecycle.TechnicalPreview => "preview", + ProductLifecycle.Beta => "beta", + ProductLifecycle.GenerallyAvailable => "ga", + ProductLifecycle.Deprecated => "deprecated", + ProductLifecycle.Removed => "removed", + ProductLifecycle.Unavailable => "unavailable", + ProductLifecycle.Development => "development", + ProductLifecycle.Planned => "planned", + ProductLifecycle.Discontinued => "discontinued", + _ => "ga" + }; + /// public override string ToString() { @@ -119,19 +192,14 @@ public override string ToString() } } -[YamlSerializable] public record DeploymentApplicability { - [YamlMember(Alias = "self")] public AppliesCollection? Self { get; set; } - [YamlMember(Alias = "ece")] public AppliesCollection? Ece { get; set; } - [YamlMember(Alias = "eck")] public AppliesCollection? Eck { get; set; } - [YamlMember(Alias = "ess")] public AppliesCollection? Ess { get; set; } public static DeploymentApplicability All { get; } = new() @@ -181,16 +249,12 @@ public override string ToString() } } -[YamlSerializable] public record ServerlessProjectApplicability { - [YamlMember(Alias = "elasticsearch")] public AppliesCollection? Elasticsearch { get; set; } - [YamlMember(Alias = "observability")] public AppliesCollection? Observability { get; set; } - [YamlMember(Alias = "security")] public AppliesCollection? Security { get; set; } /// diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs index a6dba46c4d..acafa95a4e 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs @@ -5,7 +5,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -using YamlDotNet.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.AppliesTo; @@ -154,9 +154,8 @@ public class ApplicableToJsonConverter : JsonConverter foreach (var (key, items) in productProps) { - // Find the property by YamlMember alias var property = productType.GetProperties() - .FirstOrDefault(p => p.GetCustomAttribute()?.Alias == key); + .FirstOrDefault(p => p.GetCustomAttribute()?.Name == key); property?.SetValue(productApplicability, new AppliesCollection(items.ToArray())); } @@ -209,11 +208,11 @@ public override void Write(Utf8JsonWriter writer, ApplicableTo value, JsonSerial var productType = typeof(ProductApplicability); foreach (var property in productType.GetProperties()) { - var yamlAlias = property.GetCustomAttribute()?.Alias; - if (yamlAlias != null) + var jsonName = property.GetCustomAttribute()?.Name; + if (jsonName != null) { if (property.GetValue(value.ProductApplicability) is AppliesCollection propertyValue) - WriteApplicabilityEntries(writer, "product", yamlAlias, propertyValue); + WriteApplicabilityEntries(writer, "product", jsonName, propertyValue); } } } diff --git a/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs b/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs index c486bcf47a..2c0ce7bb61 100644 --- a/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs +++ b/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs @@ -3,83 +3,82 @@ // See the LICENSE file in the project root for more information using System.Text; -using YamlDotNet.Serialization; +using System.Text.Json.Serialization; namespace Elastic.Documentation.AppliesTo; -[YamlSerializable] public record ProductApplicability { - [YamlMember(Alias = "ecctl")] + [JsonPropertyName("ecctl")] public AppliesCollection? Ecctl { get; set; } - [YamlMember(Alias = "curator")] + [JsonPropertyName("curator")] public AppliesCollection? Curator { get; set; } - [YamlMember(Alias = "apm-agent-android")] + [JsonPropertyName("apm-agent-android")] public AppliesCollection? ApmAgentAndroid { get; set; } - [YamlMember(Alias = "apm-agent-dotnet")] + [JsonPropertyName("apm-agent-dotnet")] public AppliesCollection? ApmAgentDotnet { get; set; } - [YamlMember(Alias = "apm-agent-go")] + [JsonPropertyName("apm-agent-go")] public AppliesCollection? ApmAgentGo { get; set; } - [YamlMember(Alias = "apm-agent-ios")] + [JsonPropertyName("apm-agent-ios")] public AppliesCollection? ApmAgentIos { get; set; } - [YamlMember(Alias = "apm-agent-java")] + [JsonPropertyName("apm-agent-java")] public AppliesCollection? ApmAgentJava { get; set; } - [YamlMember(Alias = "apm-agent-node")] + [JsonPropertyName("apm-agent-node")] public AppliesCollection? ApmAgentNode { get; set; } - [YamlMember(Alias = "apm-agent-php")] + [JsonPropertyName("apm-agent-php")] public AppliesCollection? ApmAgentPhp { get; set; } - [YamlMember(Alias = "apm-agent-python")] + [JsonPropertyName("apm-agent-python")] public AppliesCollection? ApmAgentPython { get; set; } - [YamlMember(Alias = "apm-agent-ruby")] + [JsonPropertyName("apm-agent-ruby")] public AppliesCollection? ApmAgentRuby { get; set; } - [YamlMember(Alias = "apm-agent-rum-js")] + [JsonPropertyName("apm-agent-rum-js")] public AppliesCollection? ApmAgentRumJs { get; set; } - [YamlMember(Alias = "edot-ios")] + [JsonPropertyName("edot-ios")] public AppliesCollection? EdotIos { get; set; } - [YamlMember(Alias = "edot-android")] + [JsonPropertyName("edot-android")] public AppliesCollection? EdotAndroid { get; set; } - [YamlMember(Alias = "edot-dotnet")] + [JsonPropertyName("edot-dotnet")] public AppliesCollection? EdotDotnet { get; set; } - [YamlMember(Alias = "edot-java")] + [JsonPropertyName("edot-java")] public AppliesCollection? EdotJava { get; set; } - [YamlMember(Alias = "edot-node")] + [JsonPropertyName("edot-node")] public AppliesCollection? EdotNode { get; set; } - [YamlMember(Alias = "edot-browser")] + [JsonPropertyName("edot-browser")] public AppliesCollection? EdotBrowser { get; set; } - [YamlMember(Alias = "edot-php")] + [JsonPropertyName("edot-php")] public AppliesCollection? EdotPhp { get; set; } - [YamlMember(Alias = "edot-python")] + [JsonPropertyName("edot-python")] public AppliesCollection? EdotPython { get; set; } - [YamlMember(Alias = "edot-cf-aws")] + [JsonPropertyName("edot-cf-aws")] public AppliesCollection? EdotCfAws { get; set; } - [YamlMember(Alias = "edot-cf-azure")] + [JsonPropertyName("edot-cf-azure")] public AppliesCollection? EdotCfAzure { get; set; } - [YamlMember(Alias = "edot-cf-gcp")] + [JsonPropertyName("edot-cf-gcp")] public AppliesCollection? EdotCfGcp { get; set; } - [YamlMember(Alias = "edot-collector")] + [JsonPropertyName("edot-collector")] public AppliesCollection? EdotCollector { get; set; } /// diff --git a/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs b/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs index 5f190b8304..c678c557d8 100644 --- a/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs +++ b/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs @@ -2,40 +2,39 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using YamlDotNet.Serialization; +using System.Text.Json.Serialization; namespace Elastic.Documentation.AppliesTo; -[YamlSerializable] public enum ProductLifecycle { // technical preview (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#beta-dev-and-preview-experimental) - [YamlMember(Alias = "preview")] + [JsonStringEnumMemberName("preview")] TechnicalPreview, // beta (ditto) - [YamlMember(Alias = "beta")] + [JsonStringEnumMemberName("beta")] Beta, // ga (replaces "added" in the current docs system since it was not entirely clear how/if that overlapped with beta/preview states) - [YamlMember(Alias = "ga")] + [JsonStringEnumMemberName("ga")] GenerallyAvailable, // deprecated (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#additions-and-deprecations) - [YamlMember(Alias = "deprecated")] + [JsonStringEnumMemberName("deprecated")] Deprecated, // removed content - [YamlMember(Alias = "removed")] + [JsonStringEnumMemberName("removed")] Removed, // unavailable (for content that doesn't exist in a specific context and is never coming or not coming anytime soon) - [YamlMember(Alias = "unavailable")] + [JsonStringEnumMemberName("unavailable")] Unavailable, // TODO remove these enum members in a future version when docs have been cleaned up // discontinued (historically we've immediately removed content when the feature ceases to be supported, but this might not be the case with pages that contain information that spans versions) - [YamlMember(Alias = "discontinued")] + [JsonStringEnumMemberName("discontinued")] Discontinued, // coming (ditto) - [YamlMember(Alias = "planned")] + [JsonStringEnumMemberName("planned")] Planned, // dev (ditto, though it's uncertain whether it's ever used or still needed) - [YamlMember(Alias = "development")] + [JsonStringEnumMemberName("development")] Development, } diff --git a/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs b/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs index ec3fc38f32..156fc94e6f 100644 --- a/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs +++ b/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO.Abstractions; -using Microsoft.Extensions.Hosting; namespace Elastic.Documentation.Diagnostics; @@ -33,11 +32,13 @@ public class DiagnosticsCollector(IReadOnlyCollection output public virtual DiagnosticsCollector StartAsync(Cancel ctx) { - _ = ((IHostedService)this).StartAsync(ctx); + _ = EnsureStarted(ctx); return this; } - Task IHostedService.StartAsync(Cancel cancellationToken) + Task IDiagnosticsCollector.StartAsync(Cancel cancellationToken) => EnsureStarted(cancellationToken); + + private Task EnsureStarted(Cancel cancellationToken) { if (_started is not null) return _started; diff --git a/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs b/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs index 6e804ad76e..7a8203fa93 100644 --- a/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs +++ b/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs @@ -4,11 +4,10 @@ using System.Collections.Concurrent; using System.IO.Abstractions; -using Microsoft.Extensions.Hosting; namespace Elastic.Documentation.Diagnostics; -public interface IDiagnosticsCollector : IAsyncDisposable, IHostedService +public interface IDiagnosticsCollector : IAsyncDisposable { int Warnings { get; } int Errors { get; } @@ -21,6 +20,9 @@ public interface IDiagnosticsCollector : IAsyncDisposable, IHostedService HashSet OffendingFiles { get; } ConcurrentDictionary InUseSubstitutionKeys { get; } + Task StartAsync(Cancel cancellationToken); + Task StopAsync(Cancel cancellationToken); + void Emit(Severity severity, string file, string message); void EmitError(string file, string message, Exception? e = null); void EmitError(string file, string message, string specificErrorMessage); diff --git a/src/Elastic.Documentation/Elastic.Documentation.csproj b/src/Elastic.Documentation/Elastic.Documentation.csproj index 3fe487e15c..cf4182443a 100644 --- a/src/Elastic.Documentation/Elastic.Documentation.csproj +++ b/src/Elastic.Documentation/Elastic.Documentation.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -11,18 +11,13 @@ - - - + + + + - - - - - - diff --git a/src/Elastic.Documentation/GitCheckoutInformation.cs b/src/Elastic.Documentation/GitCheckoutInformation.cs index a0307b267e..d1714428cd 100644 --- a/src/Elastic.Documentation/GitCheckoutInformation.cs +++ b/src/Elastic.Documentation/GitCheckoutInformation.cs @@ -2,17 +2,11 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.IO.Abstractions; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using Elastic.Documentation.Extensions; -using Microsoft.Extensions.Logging; -using Nullean.ScopedFileSystem; -using SoftCircuits.IniFileParser; namespace Elastic.Documentation; -public partial record GitCheckoutInformation +public record GitCheckoutInformation { public static GitCheckoutInformation Unavailable { get; } = new() { @@ -68,11 +62,9 @@ public partial record GitCheckoutInformation if (path is null) return null; - // Strip trailing .git if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) path = path[..^4]; - // Validate: must be exactly org/repo — two non-empty segments, no extra slashes var parts = path.Split('/'); if (parts.Length != 2 || string.IsNullOrEmpty(parts[0]) || string.IsNullOrEmpty(parts[1])) return null; @@ -85,161 +77,20 @@ public partial record GitCheckoutInformation { const string githubHost = "github.com"; - // git@github.com:org/repo.git → org/repo.git if (remote.StartsWith("git@github.com:", StringComparison.OrdinalIgnoreCase)) return remote["git@github.com:".Length..].TrimStart('/'); - // ssh://git@github.com/org/repo.git → org/repo.git if (remote.StartsWith("ssh://git@github.com/", StringComparison.OrdinalIgnoreCase)) return remote["ssh://git@github.com/".Length..].TrimStart('/'); - // https://github.com/org/repo.git or http://github.com/org/repo.git → org/repo.git if (remote.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase)) return remote["https://github.com/".Length..].TrimStart('/'); if (remote.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase)) return remote["http://github.com/".Length..].TrimStart('/'); - // Bare org/repo (e.g. GITHUB_REPOSITORY env var) — must not contain "://" or "@" (i.e. not a URL) if (!remote.Contains("://") && !remote.Contains('@') && !remote.Contains(githubHost, StringComparison.OrdinalIgnoreCase)) return remote.TrimStart('/'); return null; } - - // manual read because libgit2sharp is not yet AOT ready - public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem fileSystem, ILogger? logger = null) - { - if (source is null) - return Unavailable; - - // Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType - // (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper - // rather than relying on the outer type name. - var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType(); - if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)) - { - return new GitCheckoutInformation - { - Branch = $"test-e35fcb27-5f60-4e", - Remote = "elastic/docs-builder", - Ref = "e35fcb27-5f60-4e", - RepositoryName = "docs-builder" - }; - } - var fakeRef = Guid.NewGuid().ToString()[..16]; - - var gitDir = GitDir(source, ".git"); - if (!gitDir.Exists) - { - // try a worktree .git file - var worktreeFile = Git(source, ".git"); - if (!worktreeFile.Exists) - return Unavailable; - var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty); - if (workTreePath is null) - return Unavailable; - //TODO read branch info from worktree do not fall through - gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git"); - if (gitDir is null || !gitDir.Exists) - return Unavailable; - } - - var gitConfig = Git(gitDir, "config"); - if (!gitConfig.Exists) - { - logger?.LogInformation("Git checkout information not available."); - return Unavailable; - } - - var head = Read(gitDir, "HEAD") ?? fakeRef; - var gitRef = head; - var branch = head.Replace("refs/heads/", string.Empty); - //not detached HEAD - if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase)) - { - head = head.Replace("ref: ", string.Empty); - gitRef = Read(gitDir, head) ?? fakeRef; - branch = branch.Replace("ref: ", string.Empty); - } - else - branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head"; - - var ini = new IniFile(); - using var stream = gitConfig.OpenRead(); - using var streamReader = new StreamReader(stream); - ini.Load(streamReader); - - var remote = BranchTrackingRemote(branch, ini); - logger?.LogInformation("Remote from branch: {GitRemote}", remote); - if (string.IsNullOrEmpty(remote)) - { - remote = BranchTrackingRemote("main", ini); - logger?.LogInformation("Remote from main branch: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = BranchTrackingRemote("master", ini); - logger?.LogInformation("Remote from master branch: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); - logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = "elastic/docs-builder-unknown"; - logger?.LogInformation("Remote from fallback: {GitRemote}", remote); - } - remote = CutOffGitExtension().Replace(remote, string.Empty); - - var githubRef = Environment.GetEnvironmentVariable("GITHUB_REF"); - var info = new GitCheckoutInformation - { - Ref = gitRef, - Branch = branch, - Remote = remote, - RepositoryName = remote.Split('/').Last(), - GitHubRef = string.IsNullOrEmpty(githubRef) ? null : githubRef - }; - - logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote); - logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName); - return info; - - IFileInfo Git(IDirectoryInfo directoryInfo, string path) => - fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path)); - - IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) => - fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path)); - - string? Read(IDirectoryInfo directoryInfo, string path) - { - var gitPath = Git(directoryInfo, path).FullName; - return !fileSystem.File.Exists(gitPath) - ? null - : fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray()); - } - - string BranchTrackingRemote(string b, IniFile c) - { - var sections = c.GetSections(); - var branchSection = $"branch \"{b}\""; - if (!sections.Contains(branchSection)) - return string.Empty; - - var remoteName = ini.GetSetting(branchSection, "remote")?.Trim(); - - var remoteSection = $"remote \"{remoteName}\""; - - remote = ini.GetSetting(remoteSection, "url"); - return remote ?? string.Empty; - } - } - - [GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)] - private static partial Regex CutOffGitExtension(); } diff --git a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs b/src/Elastic.Documentation/IEnvironmentVariables.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs rename to src/Elastic.Documentation/IEnvironmentVariables.cs index 1781abe626..b79944b8bc 100644 --- a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs +++ b/src/Elastic.Documentation/IEnvironmentVariables.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation; /// /// Abstracts access to environment variables for testability. diff --git a/src/Elastic.Documentation/ChangelogEntrySubtype.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs similarity index 96% rename from src/Elastic.Documentation/ChangelogEntrySubtype.cs rename to src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs index dc51d8ce6f..f59d52833b 100644 --- a/src/Elastic.Documentation/ChangelogEntrySubtype.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation; +namespace Elastic.Documentation.ReleaseNotes; /// /// Enum representing changelog entry subtypes (only applicable to breaking changes) diff --git a/src/Elastic.Documentation/ChangelogEntryType.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs similarity index 97% rename from src/Elastic.Documentation/ChangelogEntryType.cs rename to src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs index 73c3bf9df6..32ce2ae362 100644 --- a/src/Elastic.Documentation/ChangelogEntryType.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation; +namespace Elastic.Documentation.ReleaseNotes; /// /// Enum representing changelog entry types diff --git a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs index cde2ac16ee..40595dd689 100644 --- a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs +++ b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs @@ -7,6 +7,7 @@ using Elastic.Documentation.Links; using Elastic.Documentation.Search; using Elastic.Documentation.State; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Serialization; @@ -19,6 +20,7 @@ namespace Elastic.Documentation.Serialization; [JsonSerializable(typeof(LinkRegistry))] [JsonSerializable(typeof(LinkRegistryEntry))] [JsonSerializable(typeof(DocumentationDocument))] +[JsonSerializable(typeof(AppliesToEntry))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(ApplicableTo))] [JsonSerializable(typeof(AppliesCollection))] diff --git a/src/Elastic.Documentation.Configuration/SymlinkValidator.cs b/src/Elastic.Documentation/SymlinkValidator.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/SymlinkValidator.cs rename to src/Elastic.Documentation/SymlinkValidator.cs index 0cbc6747ba..5612c577ab 100644 --- a/src/Elastic.Documentation.Configuration/SymlinkValidator.cs +++ b/src/Elastic.Documentation/SymlinkValidator.cs @@ -6,7 +6,7 @@ using System.Security; using Elastic.Documentation.Extensions; -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation; /// /// Provides validation to ensure control files (docset.yml, toc.yml, redirects.yml) diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/Versions/SemVersion.cs similarity index 99% rename from src/Elastic.Documentation/SemVersion.cs rename to src/Elastic.Documentation/Versions/SemVersion.cs index 6784bb4848..89e853f392 100644 --- a/src/Elastic.Documentation/SemVersion.cs +++ b/src/Elastic.Documentation/Versions/SemVersion.cs @@ -6,7 +6,7 @@ using System.Globalization; using System.Text.RegularExpressions; -namespace Elastic.Documentation; +namespace Elastic.Documentation.Versions; public class AllVersions() : SemVersion(99999, 0, 0) { @@ -307,4 +307,3 @@ private static int CompareComponent(string a, string b, bool lower = false) [GeneratedRegex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")] private static partial Regex MyRegex(); } - diff --git a/src/Elastic.Documentation/VersionOrDate.cs b/src/Elastic.Documentation/Versions/VersionOrDate.cs similarity index 99% rename from src/Elastic.Documentation/VersionOrDate.cs rename to src/Elastic.Documentation/Versions/VersionOrDate.cs index 7325e3c9d9..513c28c8d5 100644 --- a/src/Elastic.Documentation/VersionOrDate.cs +++ b/src/Elastic.Documentation/Versions/VersionOrDate.cs @@ -4,7 +4,7 @@ using System.Globalization; -namespace Elastic.Documentation; +namespace Elastic.Documentation.Versions; /// /// A version representation that can be either a semantic version or a date. diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/Versions/VersionSpec.cs similarity index 99% rename from src/Elastic.Documentation/VersionSpec.cs rename to src/Elastic.Documentation/Versions/VersionSpec.cs index 67881a2168..0fdf7f36ff 100644 --- a/src/Elastic.Documentation/VersionSpec.cs +++ b/src/Elastic.Documentation/Versions/VersionSpec.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; -namespace Elastic.Documentation; +namespace Elastic.Documentation.Versions; public sealed class AllVersionsSpec : VersionSpec { diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj index ed5f7a3a37..64abeb65e1 100644 --- a/src/Elastic.Markdown/Elastic.Markdown.csproj +++ b/src/Elastic.Markdown/Elastic.Markdown.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs index 1725c842a0..b44d144135 100644 --- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs +++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs @@ -127,7 +127,7 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext, StrippedBody = strippedBody, Description = fileContext.SourceFile.YamlFrontMatter?.Description, Abstract = @abstract, - Applies = appliesTo, + Applies = appliesTo.ToAppliesTo(), Parents = navigation.GetParentsOfMarkdownFile(file).Select(i => new ParentDocument { Title = i.NavigationTitle, diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs similarity index 98% rename from src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs rename to src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs index bd1fd63222..d8523168a4 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs @@ -4,10 +4,13 @@ using System.Diagnostics.CodeAnalysis; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Versions; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.AppliesTo; public class ApplicableToYamlConverter(IReadOnlyCollection productKeys) : IYamlTypeConverter diff --git a/src/Elastic.Documentation/AppliesTo/AllVersions.cs b/src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs similarity index 87% rename from src/Elastic.Documentation/AppliesTo/AllVersions.cs rename to src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs index 39b9e7325a..aecb1a3d5d 100644 --- a/src/Elastic.Documentation/AppliesTo/AllVersions.cs +++ b/src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs @@ -2,10 +2,13 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation.Versions; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.AppliesTo; public class SemVersionConverter : IYamlTypeConverter diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index a6a3dd4a6d..dcfcee4639 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -6,6 +6,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.Components; diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index 82afa2ce5f..c7b858d396 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -5,6 +5,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.Components; diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs index 954c932d5a..ae1d2f67c4 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Extensions; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Versions; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Helpers; diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs index f58e3f9f30..114f402bfe 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs @@ -6,6 +6,7 @@ using System.Text; using Elastic.Documentation; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.Directives.Changelog; diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs index 56e2d10416..4d4d7fcc6d 100644 --- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; diff --git a/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs index 6f1a8efe41..3f5a4e81d5 100644 --- a/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; diff --git a/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs b/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs index cff7043561..69ca93a98d 100644 --- a/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; diff --git a/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs b/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs index 87bab6f033..e01293cbd5 100644 --- a/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs @@ -4,6 +4,7 @@ using System.Globalization; using Elastic.Documentation; +using Elastic.Documentation.Versions; using Elastic.Markdown.Diagnostics; using static System.StringSplitOptions; diff --git a/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs b/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs index f5e696a9c3..6383623d98 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Elastic.Documentation; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.InlineParsers.Substitution; diff --git a/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs b/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs index d65d9dd06c..a2803228be 100644 --- a/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs +++ b/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs @@ -6,6 +6,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Versions; using Elastic.Markdown.Diagnostics; using Markdig; using Markdig.Parsers; diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 4f8502e75f..7e38e24f33 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -38,7 +38,6 @@ public static T Deserialize(string yaml, ProductsConfiguration products) [YamlSerializable(typeof(Setting))] [YamlSerializable(typeof(AllowedValue))] [YamlSerializable(typeof(SettingMutability))] -[YamlSerializable(typeof(ApplicableTo))] [YamlSerializable(typeof(ContributorEntry))] [YamlSerializable(typeof(ChangelogDirectiveConfigYaml))] [YamlSerializable(typeof(ChangelogDirectiveBundleConfigYaml))] diff --git a/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj index 1c60dcab23..26d0be4ac5 100644 --- a/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj +++ b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj index 9fc53017f1..f5a5b5bd5d 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj +++ b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs index c994a65ba5..2549f5b6ca 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs @@ -4,6 +4,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Microsoft.IdentityModel.Tokens; diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs index 3a0ec7e6ef..74130afdcd 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography; using System.Text.Json.Serialization; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs index 4120b13a25..cf171799a8 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Text; using System.Text.RegularExpressions; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Utilities; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs index bd1adbe4e8..cf0b061e21 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs @@ -6,7 +6,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; using Elastic.Changelog.Rendering; using Elastic.Changelog.Utilities; diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs index 3f66938565..2f867e3f1a 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs @@ -4,7 +4,6 @@ using System.IO.Abstractions; using System.Linq; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; diff --git a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs index 6d247f2a1a..7462239ee1 100644 --- a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs +++ b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs @@ -3,8 +3,8 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.Inference; diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs index a6edeb8368..f140088ad8 100644 --- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs +++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO.Abstractions; using Actions.Core.Services; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; using Elastic.Documentation.Configuration; diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs index 4308bf1e2c..aa932859dd 100644 --- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs +++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs @@ -6,9 +6,9 @@ using System.Text; using System.Text.Json; using Actions.Core.Services; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Utilities; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Services; using Microsoft.Extensions.Logging; diff --git a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs index 2351414d40..1784f98dcb 100644 --- a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs +++ b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Text; using Elastic.Changelog.Bundling; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; using Elastic.Changelog.Utilities; using Elastic.Documentation; diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs index c5d400f046..f59297ac40 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs @@ -7,7 +7,7 @@ using System.IO.Abstractions; using System.Text; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Asciidoc; diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs index 0c8188b41b..81a895618f 100644 --- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs +++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs @@ -5,13 +5,13 @@ using System.ComponentModel.DataAnnotations; using System.IO.Abstractions; using System.Text.Json.Serialization; -using Elastic.Changelog.Configuration; using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; using Elastic.Documentation.Services; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using NetEscapades.EnumGenerators; using Nullean.ScopedFileSystem; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs index 0434f2c902..1eae9745d4 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs index 38a4afe3e5..9d0e40cfdd 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs index 048612f32d..43843594d8 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs @@ -7,7 +7,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs index 73f92dee39..4531e47f9a 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs index 5b9dbe521a..b84e4945e7 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs @@ -7,7 +7,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs b/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs deleted file mode 100644 index 1599702cfb..0000000000 --- a/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using YamlDotNet.Serialization; - -namespace Elastic.Changelog.Serialization; - -[YamlStaticContext] -// YAML DTOs for CLI configuration (changelog.yml) -[YamlSerializable(typeof(ChangelogConfigurationYaml))] -[YamlSerializable(typeof(PivotConfigurationYaml))] -[YamlSerializable(typeof(TypeEntryYaml))] -[YamlSerializable(typeof(RulesConfigurationYaml))] -[YamlSerializable(typeof(CreateRulesYaml))] -[YamlSerializable(typeof(BundleRulesYaml))] -[YamlSerializable(typeof(PublishRulesYaml))] -[YamlSerializable(typeof(ProductsConfigYaml))] -[YamlSerializable(typeof(DefaultProductYaml))] -[YamlSerializable(typeof(BundleConfigurationYaml))] -[YamlSerializable(typeof(BundleProfileYaml))] -[YamlSerializable(typeof(ExtractConfigurationYaml))] -[YamlSerializable(typeof(YamlLenientList))] -public partial class ChangelogYamlStaticContext; diff --git a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs index b0baa6c81f..2c7f1bc838 100644 --- a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs +++ b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs @@ -5,7 +5,7 @@ using System.IO.Abstractions; using System.Text.RegularExpressions; using Amazon.S3; -using Elastic.Changelog.Configuration; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.ReleaseNotes; diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs index bc9615e18d..73b60ff407 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.Text; using Actions.Core.Services; +using Elastic.Documentation; using Elastic.Documentation.Assembler.Navigation; using Elastic.Documentation.Assembler.Sourcing; using Elastic.Documentation.Configuration; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs index 56da04d68b..18c621c8ad 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs @@ -43,7 +43,7 @@ public async Task ValidateLocalLinkReference(IDiagnosticsCollector collect var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null); var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); - var repository = GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName + var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName ?? throw new Exception("Unable to determine repository name"); var namespaceChecker = new NavigationPrefixChecker(logFactory, assembleContext); diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs index 4bb4bcbc63..204292009f 100644 --- a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs +++ b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using Actions.Core.Services; using Elastic.ApiExplorer; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Configuration.Inference; diff --git a/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs b/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs new file mode 100644 index 0000000000..93eb0bdf59 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs @@ -0,0 +1,28 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text.Json.Serialization; + +namespace Elastic.Documentation.Search; + +/// +/// Flat wire-format entry for the applies_to array indexed in Elasticsearch. +/// One entry per (type, sub_type, lifecycle, version) tuple, preserving the exact JSON shape +/// produced by the applicability converter in docs-builder. +/// +public record AppliesToEntry +{ + [JsonPropertyName("type")] + public required string Type { get; init; } + + [JsonPropertyName("sub_type")] + public required string SubType { get; init; } + + [JsonPropertyName("lifecycle")] + public required string Lifecycle { get; init; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; init; } +} diff --git a/src/Elastic.Documentation/Search/DocumentationDocument.cs b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs similarity index 98% rename from src/Elastic.Documentation/Search/DocumentationDocument.cs rename to src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs index 77e8f17da6..75244d6856 100644 --- a/src/Elastic.Documentation/Search/DocumentationDocument.cs +++ b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using System.Text.Json.Serialization; -using Elastic.Documentation.AppliesTo; using Elastic.Mapping; namespace Elastic.Documentation.Search; @@ -134,7 +133,7 @@ public string ContentType [Nested] [JsonPropertyName("applies_to")] - public ApplicableTo? Applies { get; set; } + public IReadOnlyCollection? Applies { get; set; } [JsonPropertyName("body")] public string? Body { get; set; } diff --git a/src/Elastic.Documentation/Search/DocumentationMappingConfig.cs b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs similarity index 100% rename from src/Elastic.Documentation/Search/DocumentationMappingConfig.cs rename to src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs diff --git a/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj b/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj new file mode 100644 index 0000000000..03a2f9155d --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + Elastic.Documentation.Search + true + true + $(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574 + + + + + + + diff --git a/src/Elastic.Documentation/Search/IndexedProduct.cs b/src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs similarity index 100% rename from src/Elastic.Documentation/Search/IndexedProduct.cs rename to src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs diff --git a/src/services/Elastic.Documentation.Search/ChangesService.cs b/src/services/search/Elastic.Documentation.Search/ChangesService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/ChangesService.cs rename to src/services/search/Elastic.Documentation.Search/ChangesService.cs diff --git a/src/services/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs rename to src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs diff --git a/src/services/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs b/src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs rename to src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs diff --git a/src/services/Elastic.Documentation.Search/Common/SearchResultProcessor.cs b/src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/Common/SearchResultProcessor.cs rename to src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs diff --git a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj similarity index 74% rename from src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj rename to src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj index 5b7ce28433..7287902825 100644 --- a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj +++ b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/services/Elastic.Documentation.Search/EsJsonContext.cs b/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/EsJsonContext.cs rename to src/services/search/Elastic.Documentation.Search/EsJsonContext.cs diff --git a/src/services/Elastic.Documentation.Search/FullSearchService.cs b/src/services/search/Elastic.Documentation.Search/FullSearchService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/FullSearchService.cs rename to src/services/search/Elastic.Documentation.Search/FullSearchService.cs diff --git a/src/services/Elastic.Documentation.Search/IChangesService.cs b/src/services/search/Elastic.Documentation.Search/IChangesService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/IChangesService.cs rename to src/services/search/Elastic.Documentation.Search/IChangesService.cs diff --git a/src/services/Elastic.Documentation.Search/IFullSearchService.cs b/src/services/search/Elastic.Documentation.Search/IFullSearchService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/IFullSearchService.cs rename to src/services/search/Elastic.Documentation.Search/IFullSearchService.cs diff --git a/src/services/Elastic.Documentation.Search/INavigationSearchService.cs b/src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/INavigationSearchService.cs rename to src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs diff --git a/src/services/Elastic.Documentation.Search/MockSearchService.cs b/src/services/search/Elastic.Documentation.Search/MockSearchService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/MockSearchService.cs rename to src/services/search/Elastic.Documentation.Search/MockSearchService.cs diff --git a/src/services/Elastic.Documentation.Search/NavigationSearchService.cs b/src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/NavigationSearchService.cs rename to src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs diff --git a/src/services/Elastic.Documentation.Search/ServicesExtension.cs b/src/services/search/Elastic.Documentation.Search/ServicesExtension.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/ServicesExtension.cs rename to src/services/search/Elastic.Documentation.Search/ServicesExtension.cs diff --git a/src/services/Elastic.Documentation.Search/SharedPointInTimeManager.cs b/src/services/search/Elastic.Documentation.Search/SharedPointInTimeManager.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/SharedPointInTimeManager.cs rename to src/services/search/Elastic.Documentation.Search/SharedPointInTimeManager.cs diff --git a/src/services/Elastic.Documentation.Search/StringHighlightExtensions.cs b/src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/StringHighlightExtensions.cs rename to src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index 909c56823e..3c6a2c5f70 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -11,7 +11,6 @@ using Documentation.Builder.Arguments; using Elastic.Changelog; using Elastic.Changelog.Bundling; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Creation; using Elastic.Changelog.Evaluation; using Elastic.Changelog.GitHub; @@ -19,7 +18,9 @@ using Elastic.Changelog.Rendering; using Elastic.Changelog.Uploading; using Elastic.Changelog.Utilities; +using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; using Elastic.Documentation.Services; diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs index adfb7ef3de..b48d33b9a0 100644 --- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs +++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs @@ -10,6 +10,7 @@ using System.Text.Json; using Documentation.Builder.Diagnostics.LiveMode; using Elastic.Documentation; +using Elastic.Documentation.Diagnostics; #if DEBUG using Elastic.Documentation.Api; #endif @@ -34,7 +35,7 @@ public class DocumentationWebHost { private readonly WebApplication _webApplication; - private readonly IHostedService _hostedService; + private readonly IDiagnosticsCollector _hostedService; private readonly ScopedFileSystem _writeFileSystem; public InMemoryBuildState InMemoryBuildState { get; } diff --git a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs index 34f41953ff..35670eadd2 100644 --- a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs +++ b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs @@ -6,6 +6,7 @@ using System.Reflection; using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using Nullean.Argh.Middleware; diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs index 95a250c570..01bb997ef0 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.Assembler.IntegrationTests; diff --git a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj index e18027f7c3..0b21ba1622 100644 --- a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj +++ b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj index c7dcf104d1..e2367742fd 100644 --- a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj +++ b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs index a2ed6c78ae..86433fd286 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs @@ -8,6 +8,7 @@ using Elastic.Documentation; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Search; +using Elastic.Documentation.Versions; using static System.StringComparison; namespace Elastic.ApiExplorer.Tests; diff --git a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs index eed99c1bcb..26727feb30 100644 --- a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs +++ b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs @@ -12,6 +12,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.ApiExplorer.Tests; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs index 712b379e35..3bfe1528c2 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs @@ -3,11 +3,11 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; -using Elastic.Changelog.Configuration; -using Elastic.Changelog.Serialization; using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; +using Elastic.Documentation.Configuration.Changelog; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs index 5e3b93a57d..832d20ab2c 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Nullean.ScopedFileSystem; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs index e00e7ddb75..1c485eedfb 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs @@ -4,6 +4,7 @@ using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs index 0f031aacb7..900013ac71 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs @@ -5,6 +5,7 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs index bd72bf9c67..03f0c54020 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs @@ -4,9 +4,9 @@ using AwesomeAssertions; using Elastic.Changelog.Bundling; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; namespace Elastic.Changelog.Tests.Changelogs.Render; diff --git a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs index bba674d2c4..ab38e30eb0 100644 --- a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs +++ b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs @@ -5,6 +5,7 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; using Elastic.Changelog.Tests.Changelogs; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs index b4f7e4fc30..60480bbc4f 100644 --- a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs +++ b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs @@ -9,6 +9,7 @@ using Elastic.Changelog.GitHub; using Elastic.Changelog.Tests.Changelogs; using Elastic.Changelog.Utilities; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj index 150e8f13af..67afbae2e4 100644 --- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs index 45cb7deaee..284b950c4a 100644 --- a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs +++ b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation; using Elastic.Documentation.Configuration; namespace Elastic.Documentation.Build.Tests; diff --git a/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs b/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs index e165b09555..87bc8daec9 100644 --- a/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs +++ b/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions.TestingHelpers; using System.Security; using AwesomeAssertions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; namespace Elastic.Documentation.Build.Tests; diff --git a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs index 51b84cd905..804502ebca 100644 --- a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs +++ b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs @@ -15,6 +15,7 @@ using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Versions; using FakeItEasy; using Microsoft.Extensions.Logging; diff --git a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs index f9c9a6ddff..7f27e0838a 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs @@ -458,6 +458,6 @@ private sealed class MockDocumentationSetContext( public IFileInfo ConfigurationPath => configurationPath; public BuildType BuildType => BuildType.Isolated; public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory; - public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem); + public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem); } } diff --git a/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs b/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs index 35a6845c14..fbece05e0c 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs @@ -6,6 +6,7 @@ using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using MatchResult = Elastic.Documentation.Configuration.Assembler.AssemblyConfiguration.ContentSourceMatch; diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs index 6c31693f72..1cb21c1e3e 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs @@ -93,6 +93,6 @@ private sealed class MockDocumentationSetContext( public IFileInfo ConfigurationPath => configurationPath; public BuildType BuildType => BuildType.Isolated; public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory; - public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem); + public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem); } } diff --git a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs index ed224f2ab8..5e34366c80 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs @@ -139,6 +139,6 @@ private sealed class MockDocumentationSetContext( public IFileInfo ConfigurationPath => configurationPath; public BuildType BuildType => BuildType.Isolated; public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory; - public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem); + public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem); } } diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs index 00a5796e9b..dd4268bb79 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Configuration.Tests; diff --git a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj index 0dc80a953d..fc32e597e0 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj +++ b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs index 4d6a17173d..172c628b19 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Configuration.Tests.ReleaseNotes; diff --git a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs index 0198135682..4ea6c22fd2 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration.Inference; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.Documentation.Configuration.Tests; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs index 66aaf303e6..d71b39d32d 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs @@ -6,6 +6,7 @@ using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.AppliesTo; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs index fd5ab13d01..376f7748ba 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs @@ -7,6 +7,7 @@ using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.AppliesTo; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs index c8700f00c6..34dbf86941 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs @@ -3,10 +3,11 @@ // See the LICENSE file in the project root for more information using System.Reflection; +using System.Text.Json.Serialization; using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; -using YamlDotNet.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.AppliesTo; @@ -19,7 +20,7 @@ public void ProductApplicabilityToStringIncludesAllProperties() var productApplicability = new ProductApplicability(); var productType = typeof(ProductApplicability); var properties = productType.GetProperties() - .Where(p => p.GetCustomAttribute() != null) + .Where(p => p.GetCustomAttribute() != null) .ToList(); // Set all properties to a test value @@ -35,9 +36,9 @@ public void ProductApplicabilityToStringIncludesAllProperties() // Verify that each property's YAML alias appears in the output foreach (var property in properties) { - var yamlAlias = property.GetCustomAttribute()!.Alias; - result.Should().Contain($"{yamlAlias}=", - $"ToString should include the property {property.Name} with alias '{yamlAlias}'"); + var jsonName = property.GetCustomAttribute()!.Name; + result.Should().Contain($"{jsonName}=", + $"ToString should include the property {property.Name} with alias '{jsonName}'"); } // Verify we have the expected number of properties @@ -90,8 +91,8 @@ public void ProductApplicabilityToStringPropertyOrderMatchesReflectionOrder() // Get the properties in reflection order var productType = typeof(ProductApplicability); var properties = productType.GetProperties() - .Where(p => p.GetCustomAttribute() != null) - .Select(p => p.GetCustomAttribute()!.Alias) + .Where(p => p.GetCustomAttribute() != null) + .Select(p => p.GetCustomAttribute()!.Name) .ToList(); // Find positions in the string diff --git a/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs b/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs index 8072ebba37..74185b46ce 100644 --- a/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs +++ b/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs @@ -6,6 +6,7 @@ using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Links; using Elastic.Documentation.Links.CrossLinks; diff --git a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs index 88f247d8bd..4b41e3e982 100644 --- a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Elastic.Documentation; +using Elastic.Documentation.Versions; using Elastic.Markdown.Myst.Directives.Version; namespace Elastic.Markdown.Tests.Directives; diff --git a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs index f09101d480..21ccbf2707 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs @@ -35,7 +35,7 @@ public class GitCheckoutInformationTests(ITestOutputHelper output) : NavigationT public void Create() { var root = ReadFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); - var git = GitCheckoutInformation.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation))); + var git = GitCheckoutInformationFactory.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation))); git.Should().NotBeNull(); git.Branch.Should().NotBeNullOrWhiteSpace(); diff --git a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj index a2a90aa4d3..5d1439526f 100644 --- a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj +++ b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs index b85bc26db0..8381feb999 100644 --- a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Search; using Elastic.Documentation.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.Search; @@ -28,7 +29,7 @@ public void SerializeDocumentWithStackAppliesToProducesCorrectJson() Applies = new ApplicableTo { Stack = AppliesCollection.GenerallyAvailable - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -66,7 +67,7 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() Ess = AppliesCollection.GenerallyAvailable, Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"3.5.0" }]) } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -108,7 +109,7 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]), Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"1.0.0" }]) } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -146,7 +147,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() Applies = new ApplicableTo { Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]) - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -180,7 +181,7 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.5.0" }]), ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"2.0.0" }]) } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -226,7 +227,7 @@ public void SerializeDocumentWithComplexAppliesToProducesCorrectJson() { Elasticsearch = AppliesCollection.GenerallyAvailable } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -279,7 +280,7 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray() Url = "/test/empty-applies", Title = "Empty Applies Test", SearchTitle = "Empty Applies Test", - Applies = new ApplicableTo() + Applies = new ApplicableTo().ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -294,6 +295,15 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray() [Fact] public void RoundTripDocumentWithAppliesToPreservesData() { + var originalApplies = new ApplicableTo + { + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]), + Deployment = new DeploymentApplicability + { + Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }]) + } + }.ToAppliesTo(); + var original = new DocumentationDocument { Type = "doc", @@ -305,14 +315,7 @@ public void RoundTripDocumentWithAppliesToPreservesData() LastUpdated = DateTimeOffset.Parse("2024-01-15T09:00:00Z", CultureInfo.InvariantCulture), ContentLastUpdated = DateTimeOffset.Parse("2024-01-14T08:00:00Z", CultureInfo.InvariantCulture), ContentBodyHash = "abc123def456abc1", - Applies = new ApplicableTo - { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]), - Deployment = new DeploymentApplicability - { - Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }]) - } - }, + Applies = originalApplies, Headings = ["Introduction", "Getting Started"], Links = ["/link1", "/link2"], Body = "Test body content", @@ -327,9 +330,9 @@ public void RoundTripDocumentWithAppliesToPreservesData() deserialized.Url.Should().Be(original.Url); deserialized.Title.Should().Be(original.Title); deserialized.Applies.Should().NotBeNull(); - deserialized.Applies.Stack.Should().BeEquivalentTo(original.Applies.Stack); - deserialized.Applies.Deployment.Should().NotBeNull(); - deserialized.Applies.Deployment.Ess.Should().BeEquivalentTo(original.Applies.Deployment.Ess); + deserialized.Applies.Should().HaveCount(2); + deserialized.Applies.Should().Contain(e => e.Type == "stack" && e.Lifecycle == "ga" && e.Version == "8.5+"); + deserialized.Applies.Should().Contain(e => e.Type == "deployment" && e.SubType == "ess" && e.Lifecycle == "beta" && e.Version == "8.6+"); deserialized.ContentLastUpdated.Should().Be(original.ContentLastUpdated); deserialized.ContentBodyHash.Should().Be(original.ContentBodyHash); deserialized.ContentType.Should().Be(original.ContentType); @@ -395,7 +398,7 @@ public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleA new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" }, new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"7.0.0" } ]) - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); diff --git a/tests/Elastic.Markdown.Tests/TestHelpers.cs b/tests/Elastic.Markdown.Tests/TestHelpers.cs index e8d9aac96a..2aa37abeab 100644 --- a/tests/Elastic.Markdown.Tests/TestHelpers.cs +++ b/tests/Elastic.Markdown.Tests/TestHelpers.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests; diff --git a/tests/authoring/Framework/Setup.fs b/tests/authoring/Framework/Setup.fs index d89f426d12..9f79710fca 100644 --- a/tests/authoring/Framework/Setup.fs +++ b/tests/authoring/Framework/Setup.fs @@ -13,6 +13,7 @@ open System.IO.Abstractions.TestingHelpers open System.Threading.Tasks open YamlDotNet.RepresentationModel open Elastic.Documentation +open Elastic.Documentation.Versions open Elastic.Documentation.Configuration open Elastic.Documentation.Configuration.LegacyUrlMappings open Elastic.Documentation.Configuration.Versions