| | | 1 | | using FastEndpoints; |
| | | 2 | | using Microsoft.AspNetCore.Http; |
| | | 3 | | |
| | | 4 | | namespace Elsa.Bpmn.Interchange.Endpoints.Bpmn; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// The error envelope every BPMN-specific refusal coded through <see cref="BpmnErrorCodes"/> is sent as. |
| | | 8 | | /// </summary> |
| | | 9 | | /// <remarks> |
| | | 10 | | /// <para> |
| | | 11 | | /// FastEndpoints has no way, in this deployment's configuration, to surface a |
| | | 12 | | /// <see cref="FluentValidation.Results.ValidationFailure.ErrorCode"/> in the error response it builds by default: |
| | | 13 | | /// that requires either its <c>ProblemDetails</c> response (which this deployment does not use — see |
| | | 14 | | /// <c>Elsa.FastEndpointConfigurators.ElsaFastEndpointsConfigurator</c>) with its <c>IndicateErrorCode</c> flag set, |
| | | 15 | | /// or replacing <c>Config.ErrOpts.ResponseBuilder</c>, which is a single, process-wide FastEndpoints setting — doing |
| | | 16 | | /// so here would reshape every endpoint's error response in the process, not just these BPMN endpoints. |
| | | 17 | | /// </para> |
| | | 18 | | /// <para> |
| | | 19 | | /// So these endpoints write this response themselves, sent through <see cref="SendAsync"/> instead of |
| | | 20 | | /// FastEndpoints' <c>Send.ErrorsAsync</c>, keeping the same top-level shape FastEndpoints' default |
| | | 21 | | /// <c>ErrorResponse</c> would have sent — <see cref="StatusCode"/>, <see cref="Message"/>, and an |
| | | 22 | | /// <see cref="Errors"/> dictionary with the same <c>generalErrors</c> key <c>AddError(message)</c> groups under — |
| | | 23 | | /// so a caller that only reads <c>message</c>/<c>errors</c> today, such as Elsa Studio's |
| | | 24 | | /// <c>ValidationApiExceptionExtensions.GetValidationErrorsFromContent</c>, keeps seeing exactly what it saw before |
| | | 25 | | /// this envelope's two additive members, <see cref="Code"/> and <see cref="Data"/>, existed. |
| | | 26 | | /// </para> |
| | | 27 | | /// </remarks> |
| | | 28 | | internal sealed class BpmnErrorResponse |
| | | 29 | | { |
| | | 30 | | /// <summary> |
| | | 31 | | /// The key FastEndpoints' own <c>AddError(message)</c> groups a message-only failure under — camelCased from |
| | | 32 | | /// its <c>Config.ErrOpts.GeneralErrorsField</c> default of <c>"GeneralErrors"</c>, which nothing in this |
| | | 33 | | /// deployment overrides (see <c>Elsa.FastEndpointConfigurators.ElsaFastEndpointsConfigurator</c>). |
| | | 34 | | /// </summary> |
| | | 35 | | private const string GeneralErrorsKey = "generalErrors"; |
| | | 36 | | |
| | | 37 | | /// <summary>The HTTP status code sent to the client.</summary> |
| | 64 | 38 | | public required int StatusCode { get; init; } |
| | | 39 | | |
| | | 40 | | /// <summary>The same default message FastEndpoints' own <c>ErrorResponse</c> carries when nothing overrides it.</su |
| | 40 | 41 | | public string Message { get; init; } = "One or more errors occurred!"; |
| | | 42 | | |
| | | 43 | | /// <summary>The same shape FastEndpoints' own <c>ErrorResponse</c> builds from an endpoint's <c>AddError</c> calls. |
| | 48 | 44 | | public required IReadOnlyDictionary<string, IReadOnlyList<string>> Errors { get; init; } |
| | | 45 | | |
| | | 46 | | /// <summary>The stable, machine-readable code identifying this refusal. See <see cref="BpmnErrorCodes"/>.</summary> |
| | 48 | 47 | | public required string Code { get; init; } |
| | | 48 | | |
| | | 49 | | /// <summary>Structured data specific to <see cref="Code"/> (e.g. the missing capability names and element ids), or |
| | 48 | 50 | | public object? Data { get; init; } |
| | | 51 | | |
| | | 52 | | /// <summary>Builds the response for a single-message refusal, in the same shape <c>AddError(message)</c> would have |
| | 24 | 53 | | public static BpmnErrorResponse Create(string message, string code, int statusCode, object? data = null) => new() |
| | 24 | 54 | | { |
| | 24 | 55 | | StatusCode = statusCode, |
| | 24 | 56 | | Errors = new Dictionary<string, IReadOnlyList<string>> { [GeneralErrorsKey] = [message] }, |
| | 24 | 57 | | Code = code, |
| | 24 | 58 | | Data = data |
| | 24 | 59 | | }; |
| | | 60 | | |
| | | 61 | | /// <summary>Sends <paramref name="response"/> with its own <see cref="StatusCode"/>.</summary> |
| | | 62 | | /// <remarks> |
| | | 63 | | /// Goes through <see cref="HttpResponse"/>'s own <c>SendAsync</c> extension rather than an endpoint's |
| | | 64 | | /// <c>Send.ErrorsAsync</c>/<c>Send.ResponseAsync</c>, since those build FastEndpoints' own <c>ErrorResponse</c> |
| | | 65 | | /// (see this type's remarks) or require the response type FastEndpoints generated for the calling endpoint's |
| | | 66 | | /// declared success response, neither of which fits an envelope with a <c>code</c> and a <c>data</c> member. |
| | | 67 | | /// <c>HttpResponse.SendAsync</c> still runs through <c>Config.SerOpts.ResponseSerializer</c> — the same |
| | | 68 | | /// <c>IApiSerializer</c>-backed serializer <c>Elsa.FastEndpointConfigurators.ElsaFastEndpointsConfigurator</c> |
| | | 69 | | /// configures for every other response — so this response's JSON casing matches the rest of the API. |
| | | 70 | | /// </remarks> |
| | | 71 | | public static Task SendAsync(HttpResponse httpResponse, BpmnErrorResponse response, CancellationToken cancellationTo |
| | 16 | 72 | | httpResponse.SendAsync(response, response.StatusCode, cancellation: cancellationToken); |
| | | 73 | | } |