| | | 1 | | using System.Collections.Frozen; |
| | | 2 | | |
| | | 3 | | namespace ZhEaIsNsAaBn.Utilities.APIResponses; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// How a failed response's HTTP status is chosen from its first error: the error <see cref="Error.Code"/> (app mapping, |
| | | 7 | | /// the defaults), then its category, <see cref="Error.Type"/> (app mapping, then the defaults), then <see cref="Fallbac |
| | | 8 | | /// Register with <c>services.AddAPIResponses( s => … )</c>; without it the defaults apply. |
| | | 9 | | /// </summary> |
| | | 10 | | /// <remarks> |
| | | 11 | | /// Defaults — codes: <c>UnAuthorized</c> 401, <c>Forbidden</c> 403, <c>NotFound</c> 404, <c>UniqueConstraintViolation</ |
| | | 12 | | /// <c>UniqueConstraint</c> / <c>ConcurrencyException</c> 409, <c>Unknown</c> 500, <c>BadGateway</c> 502, <c>GatewayTime |
| | | 13 | | /// <c>DatabaseError</c> / <c>Unknown</c> / <c>Exception</c> 500. Fallback 400. Codes and categories ignore case. |
| | | 14 | | /// </remarks> |
| | | 15 | | public sealed class APIResponsesSettings |
| | | 16 | | { |
| | 10 | 17 | | private static readonly FrozenDictionary<string, HttpStatusCode> _defaultCodes = new Dictionary<string, HttpStatusCo |
| | 10 | 18 | | { |
| | 10 | 19 | | [ "UnAuthorized" ] |
| | 10 | 20 | | [ "Forbidden" ] |
| | 10 | 21 | | [ "NotFound" ] |
| | 10 | 22 | | [ "UniqueConstraintViolatio |
| | 10 | 23 | | [ "UniqueConstraint" ] |
| | 10 | 24 | | [ "ConcurrencyException" ] |
| | 10 | 25 | | [ "Unknown" ] |
| | 10 | 26 | | [ "BadGateway" ] |
| | 10 | 27 | | [ "GatewayTimeout" ] |
| | 10 | 28 | | }.ToFrozenDictionary( StringCom |
| | | 29 | | |
| | 10 | 30 | | private static readonly FrozenDictionary<string, HttpStatusCode> _defaultCategories = new Dictionary<string, HttpSta |
| | 10 | 31 | | { |
| | 10 | 32 | | [ "Validation" ] = |
| | 10 | 33 | | [ "Business" ] = |
| | 10 | 34 | | [ "DatabaseError" ] = |
| | 10 | 35 | | [ "Unknown" ] = |
| | 10 | 36 | | [ "Exception" ] = |
| | 10 | 37 | | }.ToFrozenDictionary( Stri |
| | | 38 | | |
| | 451 | 39 | | private readonly Dictionary<string, HttpStatusCode> _categories = new( StringComparer.OrdinalIgnoreCase ); |
| | 451 | 40 | | private readonly Dictionary<string, HttpStatusCode> _codes = new( StringComparer.OrdinalIgnoreCase ); |
| | | 41 | | |
| | | 42 | | |
| | | 43 | | /// <summary>The status when neither the code nor the category is mapped. Default: 400.</summary> |
| | | 44 | | public HttpStatusCode Fallback { get; set; } = HttpStatusCode.BadRequest; |
| | | 45 | | |
| | | 46 | | |
| | | 47 | | /// <summary>Maps an error code to a status; it wins over every default and every category.</summary> |
| | | 48 | | /// <param name="code">The <see cref="Error.Code"/>, e.g. <c>NotFound</c>.</param> |
| | | 49 | | /// <param name="status">The HTTP status.</param> |
| | | 50 | | /// <returns>These settings.</returns> |
| | | 51 | | /// <exception cref="ArgumentException"><paramref name="code"/> is empty.</exception> |
| | | 52 | | public APIResponsesSettings MapCode( string code, HttpStatusCode status ) |
| | | 53 | | { |
| | 18 | 54 | | ArgumentException.ThrowIfNullOrWhiteSpace( code ); |
| | | 55 | | |
| | 18 | 56 | | _codes[ code ] = status; |
| | | 57 | | |
| | 18 | 58 | | return this; |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | /// <summary>Maps an error category (<see cref="Error.Type"/>) to a status; used when no code mapping (app or defaul |
| | | 62 | | /// <param name="category">The <see cref="Error.Type"/>, e.g. <c>DatabaseError</c>.</param> |
| | | 63 | | /// <param name="status">The HTTP status.</param> |
| | | 64 | | /// <returns>These settings.</returns> |
| | | 65 | | /// <exception cref="ArgumentException"><paramref name="category"/> is empty.</exception> |
| | | 66 | | public APIResponsesSettings MapCategory( string category, HttpStatusCode status ) |
| | | 67 | | { |
| | 14 | 68 | | ArgumentException.ThrowIfNullOrWhiteSpace( category ); |
| | | 69 | | |
| | 14 | 70 | | _categories[ category ] = status; |
| | | 71 | | |
| | 14 | 72 | | return this; |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary>The status for a failed response with <paramref name="errors"/>, from the first real error.</summary> |
| | | 76 | | /// <param name="errors">The errors; <see cref="Error.None"/> entries are skipped.</param> |
| | | 77 | | /// <returns>The status; <see cref="Fallback"/> when there is no real error.</returns> |
| | | 78 | | public HttpStatusCode StatusFor( IEnumerable<Error>? errors ) |
| | | 79 | | { |
| | 437 | 80 | | var error = errors?.FirstOrDefault( e => e is not null && e != Error.None ); |
| | | 81 | | |
| | 437 | 82 | | if ( error is null ) { return Fallback; } |
| | | 83 | | |
| | | 84 | | |
| | 717 | 85 | | if ( Find( _codes, _defaultCodes, error.Code ) is { } byCode ) { return byCode; } |
| | | 86 | | |
| | | 87 | | |
| | 157 | 88 | | return Find( _categories, _defaultCategories, error.Type ) ?? Fallback; |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | |
| | | 92 | | private static HttpStatusCode? Find( Dictionary<string, HttpStatusCode> app, FrozenDictionary<string, HttpStatusCode |
| | | 93 | | { |
| | 594 | 94 | | if ( string.IsNullOrEmpty( key ) ) { return null; } |
| | | 95 | | |
| | | 96 | | |
| | 623 | 97 | | if ( app.TryGetValue( key, out var mapped ) ) { return mapped; } |
| | | 98 | | |
| | | 99 | | |
| | 565 | 100 | | return defaults.TryGetValue( key, out var fallback ) ? fallback : null; |
| | | 101 | | } |
| | | 102 | | } |