Skip to content

Commit

Permalink
feat: create Altinn.Authorization.ProblemDetails.Abstractions
Browse files Browse the repository at this point in the history
  • Loading branch information
Alxandr committed May 24, 2024
1 parent adfb1c5 commit 7ff8f2e
Show file tree
Hide file tree
Showing 25 changed files with 1,002 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{25B3E858-1E7
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.ProblemDetails", "src\ProblemDetails\Altinn.Authorization.ProblemDetails.csproj", "{190CF604-A972-4003-8E67-38169F3D57CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.ProblemDetails.Abstractions", "src\ProblemDetails.Abstractions\Altinn.Authorization.ProblemDetails.Abstractions.csproj", "{72FCCF0A-3C5F-4FEA-8ED2-F6A8B2CCB859}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -28,9 +30,14 @@ Global
{190CF604-A972-4003-8E67-38169F3D57CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{190CF604-A972-4003-8E67-38169F3D57CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{190CF604-A972-4003-8E67-38169F3D57CB}.Release|Any CPU.Build.0 = Release|Any CPU
{72FCCF0A-3C5F-4FEA-8ED2-F6A8B2CCB859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72FCCF0A-3C5F-4FEA-8ED2-F6A8B2CCB859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72FCCF0A-3C5F-4FEA-8ED2-F6A8B2CCB859}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72FCCF0A-3C5F-4FEA-8ED2-F6A8B2CCB859}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C1E314C3-2010-47B4-818B-719F1D1A2540} = {E495A322-353E-44BE-ADEE-4DB514621BBF}
{190CF604-A972-4003-8E67-38169F3D57CB} = {25B3E858-1E75-4314-8F6A-99B1E2170709}
{72FCCF0A-3C5F-4FEA-8ED2-F6A8B2CCB859} = {25B3E858-1E75-4314-8F6A-99B1E2170709}
EndGlobalSection
EndGlobal
73 changes: 57 additions & 16 deletions src/Altinn.Authorization.ProblemDetails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,83 @@ dotnet add package Altinn.Authorization.ProblemDetails

## Usage

### Example
### ProblemDetails

This library allows for defining custom errors that contain error codes usable by clients to determine what went wrong. This is done by creating custom `ProblemDescriptor`s, which can trivially be converted into `ProblemDetail`s by calling `ToProblemDetails()` on them.

#### ProblemDetails Example

Here's a basic example demonstrating how to use the `Altinn.Authorization.ProblemDetails` library:

```csharp
internal static class MyAppErrors
{
private static readonly AltinnProblemDetailsFactory _factory
= AltinnProblemDetailsFactory.New("APP");
private static readonly ProblemDescriptorFactory _factory
= ProblemDescriptorFactory.New("APP");

public static AltinnProblemDetails InvalidUser
=> _factory.Create(1, HttpStatusCode.BadRequest, "Provided user is not valid");
public static ProblemDescriptor BadRequest { get; }
= _factory.Create(0, HttpStatusCode.BadRequest, "Bad request");

public static AltinnProblemDetails OrganizationNotFound
=> _factory.Create(2, HttpStatusCode.NotFound, "The specified organization was not found");
public static ProblemDescriptor NotFound { get; }
= _factory.Create(1, HttpStatusCode.NotFound, "Not found");

public static AltinnProblemDetails InternalServerError
=> _factory.Create(3, HttpStatusCode.InternalServerError, "Internal server error");
public static ProblemDescriptor InternalServerError { get; }
= _factory.Create(2, HttpStatusCode.InternalServerError, "Internal server error");

public static AltinnProblemDetails NotImplemented
=> _factory.Create(4, HttpStatusCode.NotImplemented, "Not implemented");
public static ProblemDescriptor NotImplemented { get; }
= _factory.Create(3, HttpStatusCode.NotImplemented, "Not implemented");
}
```

### Explanation
#### Explanation

- `ProblemDescriptorFactory`: This class provides a factory method `New()` to create a new instance of `ProblemDescriptorFactory`.

- `Create()`: This method is used to create a new `ProblemDescriptor` object with a custom error code, HTTP status code, and error message. These can then be turned into `ProblemDetails` objects by calling `ToProblemDetails()`.

### Validation Errors

- `AltinnProblemDetailsFactory`: This class provides a factory method `New()` to create a new instance of `AltinnProblemDetailsFactory`.
A predefined `AltinnValidationProblemDetails` is provided for the case where you have one or more validation errors that should be returned to the client. This variant of `ProblemDetails` takes a list of validation errors, which can be created in a similar fasion to `ProblemDescriptor`s.

#### ValidationErrors Example

Here's a basic example demonstrating how to create custom validation errors:

```csharp
internal static class MyAppValidationDescriptors
{
private static readonly ValidationErrorDescriptorFactory _factory
= ValidationErrorDescriptorFactory.New("APP");

public static ValidationErrorDescriptor FieldRequired { get; }
= _factory.Create(0, "Field is required.");

public static ValidationErrorDescriptor FieldOutOfRange { get; }
= _factory.Create(1, "Field is out of range.");

public static ValidationErrorDescriptor PasswordsMustMatch { get; }
= _factory.Create(2, "Passwords must match.");
}
```

And how to use them:

```csharp
var details = new AltinnValidationProblemDetails([
MyAppValidationDescriptors.FieldRequired.ToValidationError("/field1"),
MyAppValidationDescriptors.FieldRequired.ToValidationError("/field2"),
MyAppValidationDescriptors.PasswordsMustMatch.ToValidationError(["/password", "/confirmPassword"]),
]);
```

- `Create()`: This method is used to create a new `ProblemDetails` object with a custom error code, HTTP status code, and error message.
A set of common validation errors are also provided through the `StdValidationErrors` class.

### Customization

You can customize the prefix used for error codes by passing a custom prefix to the `New()` method of `AltinnProblemDetailsFactory`.
You can customize the prefix used for error codes by passing a custom prefix to the `New()` method of `ProblemDescriptorFactory`. All application-domains should have their own prefix.

```csharp
AltinnProblemDetailsFactory.New("PFX");
ProblemDescriptorFactory.New("PFX");
```

The prefix is required to be only uppercase ASCII letters of either 2, 3, or 4 characters in length.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RootNamespace>Altinn.Authorization.ProblemDetails</RootNamespace>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Altinn.Authorization.ProblemDetails" />
<InternalsVisibleTo Include="Altinn.Authorization.ProblemDetails.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public readonly struct ErrorCode
internal const int MIN_LENGTH = ErrorCodeDomain.MIN_LENGTH + 1 + NUM_LENGTH;
internal const int MAX_LENGTH = ErrorCodeDomain.MAX_LENGTH + 1 + NUM_LENGTH;
private static readonly SearchValues<char> VALID_CHARS
= SearchValues.Create("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
= SearchValues.Create("-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");

private readonly string? _value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ namespace Altinn.Authorization.ProblemDetails;
[DebuggerDisplay("Domain = {Name}")]
internal sealed class ErrorCodeDomain
{
internal const int MIN_LENGTH = 2;
internal const int MAX_LENGTH = 4;
internal const int ROOT_MIN_LENGTH = 2;
internal const int ROOT_MAX_LENGTH = 4;

internal const int SUB_MIN_LENGTH = 1;
internal const int SUB_MAX_LENGTH = 4;

internal const int MIN_LENGTH = ROOT_MIN_LENGTH;
internal const int MAX_LENGTH = ROOT_MAX_LENGTH + SUB_MAX_LENGTH + 1;

private static readonly SearchValues<char> VALID_CHARS
= SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZ");

Expand All @@ -32,6 +39,10 @@ private static ErrorCodeDomain Create(string name)
=> new ErrorCodeDomain(name);

private readonly string _name;
private readonly ErrorCodeDomain _root;

private ImmutableDictionary<string, ErrorCodeDomain> _subDomains
= ImmutableDictionary<string, ErrorCodeDomain>.Empty;

private ErrorCodeDomain(string name)
{
Expand All @@ -45,10 +56,44 @@ private ErrorCodeDomain(string name)
}

_name = name;
_root = this;
}

private ErrorCodeDomain(string name, ErrorCodeDomain root)
{
Guard.IsNotNullOrWhiteSpace(name);
Guard.HasSizeLessThanOrEqualTo(name, SUB_MAX_LENGTH);
Guard.HasSizeGreaterThanOrEqualTo(name, SUB_MIN_LENGTH);

if (name.AsSpan().ContainsAnyExcept(VALID_CHARS))
{
ThrowHelper.ThrowArgumentException(nameof(name), "Domain name must be uppercase ASCII letters only.");
}

_name = $"{root.Name}.{name}";
_root = root;
}

internal string Name => _name;

/// <summary>
/// Gets a subdomain of this domain.
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
internal ErrorCodeDomain SubDomain(string name)
{
if (!ReferenceEquals(this, _root))
{
ThrowHelper.ThrowInvalidOperationException("Subdomains can only be created from root domains.");
}

return ImmutableInterlocked.GetOrAdd(ref _subDomains, name, CreateSubDomain);
}

private ErrorCodeDomain CreateSubDomain(string name)
=> new ErrorCodeDomain(name, this);

/// <summary>
/// Creates a new <see cref="ErrorCode"/> for this domain.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using CommunityToolkit.Diagnostics;
using System.Net;

namespace Altinn.Authorization.ProblemDetails;

/// <summary>
/// An immutable descriptor for a problem.
/// </summary>
public sealed class ProblemDescriptor
{
/// <summary>
/// Gets the error code.
/// </summary>
public ErrorCode ErrorCode { get; }

/// <summary>
/// Gets the status code.
/// </summary>
public HttpStatusCode StatusCode { get; }

/// <summary>
/// Gets the error details.
/// </summary>
public string Detail { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ProblemDescriptor"/> class.
/// </summary>
/// <param name="errorCode">The error code.</param>
/// <param name="statusCode">The HTTP status code.</param>
/// <param name="detail">The error description.</param>
internal ProblemDescriptor(ErrorCode errorCode, HttpStatusCode statusCode, string detail)
{
Guard.IsNotDefault(errorCode);
Guard.IsNotNullOrWhiteSpace(detail);

ErrorCode = errorCode;
StatusCode = statusCode;
Detail = detail;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Net;

namespace Altinn.Authorization.ProblemDetails;

/// <summary>
/// A factory for creating <see cref="ProblemDescriptor"/>s.
/// </summary>
public sealed class ProblemDescriptorFactory
{
/// <summary>
/// Creates a new <see cref="ProblemDescriptorFactory"/> for a given domain name.
/// </summary>
/// <param name="domainName">The domain name.</param>
/// <returns>A <see cref="ProblemDescriptorFactory"/>.</returns>
/// <remarks>Domain names must be 2-4 letter ASCII uppercase.</remarks>
public static ProblemDescriptorFactory New(string domainName)
=> new(ErrorCodeDomain.Get(domainName));

private readonly ErrorCodeDomain _domain;

private ProblemDescriptorFactory(ErrorCodeDomain domain)
{
_domain = domain;
}

/// <summary>
/// Creates a new <see cref="ProblemDescriptor"/>.
/// </summary>
/// <param name="code">The (domain specific) error code.</param>
/// <param name="statusCode">The <see cref="HttpStatusCode"/> for the error.</param>
/// <param name="detail">The error details (message).</param>
/// <returns>A newly created <see cref="ProblemDescriptor"/>.</returns>
public ProblemDescriptor Create(uint code, HttpStatusCode statusCode, string detail)
=> new ProblemDescriptor(_domain.Code(code), statusCode, detail);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Net;

namespace Altinn.Authorization.ProblemDetails;

/// <summary>
/// Standard problem descriptors.
/// </summary>
public static class StdProblemDescriptors
{
internal const string DOMAIN_NAME = "STD";

private static readonly ProblemDescriptorFactory _factory
= ProblemDescriptorFactory.New(DOMAIN_NAME);

/// <summary>
/// Gets a problem descriptor for a validation error.
/// </summary>
/// <remarks>
/// This property should remain internal to avoid direct use. To create a validation error
/// use the AltinnValidationProblemDetails class from the Altinn.Authorization.ProblemDetails project.
/// </remarks>
internal static ProblemDescriptor ValidationError { get; }
= _factory.Create(0, HttpStatusCode.BadRequest, "One or more validation errors occurred.");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Altinn.Authorization.ProblemDetails;

/// <summary>
/// Standard validation errors.
/// </summary>
public static class StdValidationErrors
{
private static readonly ValidationErrorDescriptorFactory _factory
= ValidationErrorDescriptorFactory.New(StdProblemDescriptors.DOMAIN_NAME);

/// <summary>
/// Gets a validation error descriptor for a required field missing.
/// </summary>
public static ValidationErrorDescriptor Required { get; }
= _factory.Create(0, "The field is required.");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using CommunityToolkit.Diagnostics;

namespace Altinn.Authorization.ProblemDetails;

/// <summary>
/// An immutable descriptor for a validation error.
/// </summary>
public sealed class ValidationErrorDescriptor
{
/// <summary>
/// Gets the error code.
/// </summary>
public ErrorCode ErrorCode { get; }

/// <summary>
/// Gets the error details.
/// </summary>
public string Detail { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ValidationErrorDescriptor"/> class.
/// </summary>
/// <param name="errorCode">The error code.</param>
/// <param name="detail">The error description.</param>
internal ValidationErrorDescriptor(ErrorCode errorCode, string detail)
{
Guard.IsNotDefault(errorCode);
Guard.IsNotNullOrWhiteSpace(detail);

ErrorCode = errorCode;
Detail = detail;
}
}
Loading

0 comments on commit 7ff8f2e

Please sign in to comment.