| | | 1 | | using Bpmn.Interchange; |
| | | 2 | | using Bpmn.Semantics; |
| | | 3 | | using Elsa.Bpmn.Interchange.Exceptions; |
| | | 4 | | using Elsa.Bpmn.Interchange.Services; |
| | | 5 | | using Microsoft.AspNetCore.Http; |
| | | 6 | | |
| | | 7 | | namespace Elsa.Bpmn.Interchange.Endpoints.Bpmn; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// The exception-to-response mapping shared by every endpoint that imports a BPMN document through |
| | | 11 | | /// <c>BpmnInterchangeDocumentService</c> — <c>Import</c> and the document <c>Put</c> endpoint — plus the identical |
| | | 12 | | /// handling both give a result whose <c>ImportResult</c> did not succeed. |
| | | 13 | | /// </summary> |
| | | 14 | | internal static class BpmnImportErrorResponses |
| | | 15 | | { |
| | | 16 | | /// <summary> |
| | | 17 | | /// Runs <paramref name="import"/>, reporting the shared error response for whichever exception it throws, or for |
| | | 18 | | /// an unsuccessful <see cref="BpmnDocumentImportResult.ImportResult"/>. A refusal this type gives a |
| | | 19 | | /// <see cref="BpmnErrorCodes"/> code goes out through <paramref name="httpResponse"/> as a |
| | | 20 | | /// <see cref="BpmnErrorResponse"/>; the remaining, uncoded refusals still go through <paramref name="addError"/> |
| | | 21 | | /// and <paramref name="sendErrorsAsync"/>, exactly as before. Returns <c>null</c> in every case that already sent |
| | | 22 | | /// a response; the caller sends its own success response otherwise. |
| | | 23 | | /// </summary> |
| | | 24 | | public static async Task<BpmnDocumentImportResult?> RunAsync( |
| | | 25 | | Func<Task<BpmnDocumentImportResult>> import, |
| | | 26 | | HttpResponse httpResponse, |
| | | 27 | | Action<string> addError, |
| | | 28 | | Func<int, CancellationToken, Task> sendErrorsAsync, |
| | | 29 | | CancellationToken cancellationToken) |
| | | 30 | | { |
| | | 31 | | BpmnDocumentImportResult result; |
| | | 32 | | |
| | | 33 | | try |
| | | 34 | | { |
| | 57 | 35 | | result = await import(); |
| | 49 | 36 | | } |
| | 0 | 37 | | catch (BpmnDefinitionNotFoundException exception) |
| | | 38 | | { |
| | 0 | 39 | | await BpmnErrorResponse.SendAsync(httpResponse, NotFoundResponseFor(exception), cancellationToken); |
| | 0 | 40 | | return null; |
| | | 41 | | } |
| | 0 | 42 | | catch (BpmnDocumentPreconditionFailedException exception) |
| | | 43 | | { |
| | 0 | 44 | | await BpmnErrorResponse.SendAsync(httpResponse, PreconditionFailedResponseFor(exception), cancellationToken) |
| | 0 | 45 | | return null; |
| | | 46 | | } |
| | 1 | 47 | | catch (BpmnInterchangeException exception) |
| | | 48 | | { |
| | 1 | 49 | | addError(exception.Message); |
| | 1 | 50 | | await sendErrorsAsync(StatusCodes.Status400BadRequest, cancellationToken); |
| | 1 | 51 | | return null; |
| | | 52 | | } |
| | 2 | 53 | | catch (BpmnBindingException exception) |
| | | 54 | | { |
| | 2 | 55 | | await BpmnErrorResponse.SendAsync(httpResponse, BindingInvalidResponseFor(exception), cancellationToken); |
| | 2 | 56 | | return null; |
| | | 57 | | } |
| | 5 | 58 | | catch (BpmnDuplicateElementIdException exception) |
| | | 59 | | { |
| | 5 | 60 | | await BpmnErrorResponse.SendAsync(httpResponse, DuplicateElementIdResponseFor(exception), cancellationToken) |
| | 5 | 61 | | return null; |
| | | 62 | | } |
| | 0 | 63 | | catch (BpmnCapabilityException exception) |
| | | 64 | | { |
| | 0 | 65 | | await BpmnErrorResponse.SendAsync(httpResponse, CapabilityResponseFor(exception), cancellationToken); |
| | 0 | 66 | | return null; |
| | | 67 | | } |
| | | 68 | | |
| | 49 | 69 | | if (!result.ImportResult.Succeeded) |
| | | 70 | | { |
| | 0 | 71 | | foreach (var validationError in result.ImportResult.ValidationErrors) |
| | 0 | 72 | | addError(validationError.Message); |
| | | 73 | | |
| | 0 | 74 | | await sendErrorsAsync(StatusCodes.Status400BadRequest, cancellationToken); |
| | 0 | 75 | | return null; |
| | | 76 | | } |
| | | 77 | | |
| | 49 | 78 | | return result; |
| | 57 | 79 | | } |
| | | 80 | | |
| | | 81 | | /// <summary> |
| | | 82 | | /// The <see cref="BpmnErrorCodes.DocumentNotFound"/> response for <paramref name="exception"/>, as its own pure, |
| | | 83 | | /// synchronous step so it can be asserted on directly: the endpoints that can throw |
| | | 84 | | /// <see cref="BpmnDefinitionNotFoundException"/> already refuse the ordinary "no such definition" case earlier, |
| | | 85 | | /// through their own existence check, so reaching this from a real request needs the definition to be deleted in |
| | | 86 | | /// the narrow window between that check and the import call this wraps — not something an HTTP-level test can |
| | | 87 | | /// reliably force without a race. |
| | | 88 | | /// </summary> |
| | | 89 | | internal static BpmnErrorResponse NotFoundResponseFor(BpmnDefinitionNotFoundException exception) => |
| | 1 | 90 | | BpmnErrorResponse.Create(exception.Message, BpmnErrorCodes.DocumentNotFound, StatusCodes.Status404NotFound); |
| | | 91 | | |
| | | 92 | | /// <summary> |
| | | 93 | | /// The <see cref="BpmnErrorCodes.DocumentPreconditionFailed"/> response for a lost compare-and-swap — |
| | | 94 | | /// the same code and status the PUT already sends when <c>If-Match</c> is stale on arrival. |
| | | 95 | | /// </summary> |
| | | 96 | | internal static BpmnErrorResponse PreconditionFailedResponseFor(BpmnDocumentPreconditionFailedException exception) = |
| | 1 | 97 | | BpmnErrorResponse.Create(exception.Message, BpmnErrorCodes.DocumentPreconditionFailed, StatusCodes.Status412Prec |
| | | 98 | | |
| | | 99 | | /// <summary>The <see cref="BpmnErrorCodes.ImportBindingInvalid"/> response for <paramref name="exception"/>.</summa |
| | | 100 | | internal static BpmnErrorResponse BindingInvalidResponseFor(BpmnBindingException exception) => |
| | 3 | 101 | | BpmnErrorResponse.Create(exception.Message, BpmnErrorCodes.ImportBindingInvalid, StatusCodes.Status422Unprocessa |
| | | 102 | | |
| | | 103 | | /// <summary> |
| | | 104 | | /// The <see cref="BpmnErrorCodes.ImportDuplicateElementId"/> response for <paramref name="exception"/>, carrying |
| | | 105 | | /// the duplicated ids as <c>data.elementIds</c>. |
| | | 106 | | /// </summary> |
| | | 107 | | internal static BpmnErrorResponse DuplicateElementIdResponseFor(BpmnDuplicateElementIdException exception) => |
| | 6 | 108 | | BpmnErrorResponse.Create( |
| | 6 | 109 | | exception.Message, |
| | 6 | 110 | | BpmnErrorCodes.ImportDuplicateElementId, |
| | 6 | 111 | | StatusCodes.Status422UnprocessableEntity, |
| | 6 | 112 | | new { ElementIds = exception.DuplicateElementIds }); |
| | | 113 | | |
| | | 114 | | /// <summary> |
| | | 115 | | /// The <see cref="BpmnErrorCodes.ImportCapabilityUnsupported"/> response for <paramref name="exception"/>, as its |
| | | 116 | | /// own pure, synchronous step so it can be asserted on directly: <c>Bpmn.*</c> 0.2.0 declares every capability |
| | | 117 | | /// this deployment's runtime needs, so nothing in <c>Elsa.Bpmn.Interchange.IntegrationTests</c> can currently |
| | | 118 | | /// make a real import throw <see cref="BpmnCapabilityException"/> to exercise this through the endpoint itself. |
| | | 119 | | /// </summary> |
| | | 120 | | internal static BpmnErrorResponse CapabilityResponseFor(BpmnCapabilityException exception) => |
| | 1 | 121 | | BpmnErrorResponse.Create( |
| | 1 | 122 | | BpmnCapabilityErrorFormatter.Format(exception), |
| | 1 | 123 | | BpmnErrorCodes.ImportCapabilityUnsupported, |
| | 1 | 124 | | StatusCodes.Status422UnprocessableEntity, |
| | 1 | 125 | | BpmnCapabilityErrorFormatter.DataFor(exception)); |
| | | 126 | | } |