1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 00:52:49 -05:00

CommandResult and ValidationResult tweaks (#5772)

* Simplify and align CommandResult and ValidationResult.
  In particular, 1 error per Failure/Invalid.

* Move these files to a common namespace

* Remove unused code
This commit is contained in:
Thomas Rittson
2025-05-09 10:46:49 +10:00
committed by GitHub
parent 547df25045
commit 5b3d3d6e20
42 changed files with 137 additions and 386 deletions

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.Models.Commands;
using Bit.Core.AdminConsole.Utilities.Commands;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;

View File

@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Utilities.Errors;
using Bit.Core.Exceptions;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Models.Commands;
using Bit.Core.AdminConsole.Utilities.Commands;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;

View File

@ -1,17 +1,17 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Commands;
using Bit.Core.AdminConsole.Utilities.Errors;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Commands;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -50,11 +50,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
{
case Failure<InviteOrganizationUsersResponse> failure:
return new Failure<ScimInviteOrganizationUsersResponse>(
failure.Errors.Select(error => new Error<ScimInviteOrganizationUsersResponse>(error.Message,
new Error<ScimInviteOrganizationUsersResponse>(failure.Error.Message,
new ScimInviteOrganizationUsersResponse
{
InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault()
})));
InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault()
}));
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
var user = success.Value.InvitedUsers.First();

View File

@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;

View File

@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Errors;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;

View File

@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;

View File

@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;

View File

@ -1,6 +1,6 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;

View File

@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Billing.Extensions;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.Utilities.Commands;
using Bit.Core.Enums;
using Bit.Core.Models.Commands;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;

View File

@ -1,44 +0,0 @@
using Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.Shared.Validation;
public abstract record ValidationResult<T>;
public record Valid<T> : ValidationResult<T>
{
public Valid() { }
public Valid(T Value)
{
this.Value = Value;
}
public T Value { get; init; }
}
public record Invalid<T> : ValidationResult<T>
{
public IEnumerable<Error<T>> Errors { get; init; } = [];
public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message));
public Invalid() { }
public Invalid(Error<T> error) : this([error]) { }
public Invalid(IEnumerable<Error<T>> errors)
{
Errors = errors;
}
}
public static class ValidationResultMappers
{
public static ValidationResult<B> Map<A, B>(this ValidationResult<A> validationResult, B invalidValue) =>
validationResult switch
{
Valid<A> => new Valid<B>(invalidValue),
Invalid<A> invalid => new Invalid<B>(invalid.Errors.Select(x => x.ToError(invalidValue))),
_ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type")
};
}

View File

@ -0,0 +1,51 @@
#nullable enable
using Bit.Core.AdminConsole.Utilities.Errors;
using Bit.Core.AdminConsole.Utilities.Validation;
namespace Bit.Core.AdminConsole.Utilities.Commands;
public abstract class CommandResult<T>;
public class Success<T>(T value) : CommandResult<T>
{
public T Value { get; } = value;
}
public class Failure<T>(Error<T> error) : CommandResult<T>
{
public Error<T> Error { get; } = error;
}
public class Partial<T>(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
: CommandResult<T>
{
public IEnumerable<T> Successes { get; } = successfulItems;
public IEnumerable<Error<T>> Failures { get; } = failedItems;
}
public static class CommandResultExtensions
{
/// <summary>
/// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types.
///
/// </summary>
/// <param name="invalidResult">This is the invalid type from validating the object.</param>
/// <param name="mappingFunction">This function will map between the two types for the inner ErrorT</param>
/// <typeparam name="A">Invalid object's type</typeparam>
/// <typeparam name="B">Failure object's type</typeparam>
/// <returns></returns>
public static CommandResult<B> MapToFailure<A, B>(this Invalid<A> invalidResult, Func<A, B> mappingFunction) =>
new Failure<B>(invalidResult.Error.ToError(mappingFunction(invalidResult.Error.ErroredValue)));
}
[Obsolete("Use CommandResult<T> instead. This will be removed once old code is updated.")]
public class CommandResult(IEnumerable<string> errors)
{
public CommandResult(string error) : this([error]) { }
public bool Success => ErrorMessages.Count == 0;
public bool HasErrors => ErrorMessages.Count > 0;
public List<string> ErrorMessages { get; } = errors.ToList();
public CommandResult() : this(Array.Empty<string>()) { }
}

View File

@ -1,4 +1,4 @@
namespace Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.Utilities.Errors;
public record Error<T>(string Message, T ErroredValue);

View File

@ -1,4 +1,4 @@
namespace Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.Utilities.Errors;
public record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
{

View File

@ -1,4 +1,4 @@
namespace Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.Utilities.Errors;
public record InvalidResultTypeError<T>(T Value) : Error<T>(Code, Value)
{

View File

@ -1,4 +1,4 @@
namespace Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.Utilities.Errors;
public record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
{

View File

@ -1,4 +1,4 @@
namespace Bit.Core.AdminConsole.Shared.Validation;
namespace Bit.Core.AdminConsole.Utilities.Validation;
public interface IValidator<T>
{

View File

@ -0,0 +1,20 @@
using Bit.Core.AdminConsole.Utilities.Errors;
namespace Bit.Core.AdminConsole.Utilities.Validation;
public abstract record ValidationResult<T>;
public record Valid<T>(T Value) : ValidationResult<T>;
public record Invalid<T>(Error<T> Error) : ValidationResult<T>;
public static class ValidationResultMappers
{
public static ValidationResult<B> Map<A, B>(this ValidationResult<A> validationResult, B invalidValue) =>
validationResult switch
{
Valid<A> => new Valid<B>(invalidValue),
Invalid<A> invalid => new Invalid<B>(invalid.Error.ToError(invalidValue)),
_ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type")
};
}

View File

@ -1,23 +0,0 @@
namespace Bit.Core.Models.Commands;
public class BadRequestFailure<T> : Failure<T>
{
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
{
}
public BadRequestFailure(string errorMessage) : base(errorMessage)
{
}
}
public class BadRequestFailure : Failure
{
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
{
}
public BadRequestFailure(string errorMessage) : base(errorMessage)
{
}
}

View File

@ -1,88 +0,0 @@
#nullable enable
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Shared.Validation;
namespace Bit.Core.Models.Commands;
public class CommandResult(IEnumerable<string> errors)
{
public CommandResult(string error) : this([error]) { }
public bool Success => ErrorMessages.Count == 0;
public bool HasErrors => ErrorMessages.Count > 0;
public List<string> ErrorMessages { get; } = errors.ToList();
public CommandResult() : this(Array.Empty<string>()) { }
}
public class Failure : CommandResult
{
protected Failure(IEnumerable<string> errorMessages) : base(errorMessages)
{
}
public Failure(string errorMessage) : base(errorMessage)
{
}
}
public class Success : CommandResult
{
}
public abstract class CommandResult<T>;
public class Success<T>(T value) : CommandResult<T>
{
public T Value { get; } = value;
}
public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
{
public List<string> ErrorMessages { get; } = errorMessages.ToList();
public Error<T>[] Errors { get; set; } = [];
public string ErrorMessage => string.Join(" ", ErrorMessages);
public Failure(string error) : this([error])
{
}
public Failure(IEnumerable<Error<T>> errors) : this(errors.Select(e => e.Message))
{
Errors = errors.ToArray();
}
public Failure(Error<T> error) : this([error.Message])
{
Errors = [error];
}
}
public class Partial<T> : CommandResult<T>
{
public T[] Successes { get; set; } = [];
public Error<T>[] Failures { get; set; } = [];
public Partial(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
{
Successes = successfulItems.ToArray();
Failures = failedItems.ToArray();
}
}
public static class CommandResultExtensions
{
/// <summary>
/// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types.
///
/// </summary>
/// <param name="invalidResult">This is the invalid type from validating the object.</param>
/// <param name="mappingFunction">This function will map between the two types for the inner ErrorT</param>
/// <typeparam name="A">Invalid object's type</typeparam>
/// <typeparam name="B">Failure object's type</typeparam>
/// <returns></returns>
public static CommandResult<B> MapToFailure<A, B>(this Invalid<A> invalidResult, Func<A, B> mappingFunction) =>
new Failure<B>(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue))));
}

View File

@ -1,24 +0,0 @@
namespace Bit.Core.Models.Commands;
public class NoRecordFoundFailure<T> : Failure<T>
{
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
{
}
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
{
}
}
public class NoRecordFoundFailure : Failure
{
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
{
}
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
{
}
}