From 93399f287edf4781a42fae480c4bbe330d8141a4 Mon Sep 17 00:00:00 2001 From: andreastanderen Date: Mon, 11 Nov 2024 15:49:16 +0100 Subject: [PATCH] Add custom attribute for OptionValue and OptionLabel and allow empty string --- .../Options/InvalidOptionsFormatException.cs | 25 +++++ .../Filters/Options/OptionsErrorCodes.cs | 6 ++ .../OptionsExceptionFilterAttribute.cs | 26 ++++++ .../NotNullableAttribute.cs | 15 +++ .../OptionConverterHelper.cs | 7 +- .../Infrastructure/MvcConfiguration.cs | 2 + backend/src/Designer/Models/Option.cs | 7 +- .../Services/Implementation/OptionsService.cs | 31 +++++-- .../OptionsController/UpdateOptionsTests.cs | 93 ++++++++++++------- 9 files changed, 160 insertions(+), 52 deletions(-) create mode 100644 backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs create mode 100644 backend/src/Designer/Filters/Options/OptionsErrorCodes.cs create mode 100644 backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs create mode 100644 backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs diff --git a/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs b/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs new file mode 100644 index 00000000000..0dc69bc495d --- /dev/null +++ b/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Altinn.Studio.Designer.Exceptions.Options; + +/// +/// Indicates that an error occurred during json serialization of options. +/// +[Serializable] +public class InvalidOptionsFormatException : Exception +{ + /// + public InvalidOptionsFormatException() + { + } + + /// + public InvalidOptionsFormatException(string message) : base(message) + { + } + + /// + public InvalidOptionsFormatException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs b/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs new file mode 100644 index 00000000000..9053087b1b5 --- /dev/null +++ b/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs @@ -0,0 +1,6 @@ +namespace Altinn.Studio.Designer.Filters.Options; + +public class OptionsErrorCodes +{ + public const string InvalidOptionsFormat = nameof(InvalidOptionsFormat); +} diff --git a/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs b/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs new file mode 100644 index 00000000000..ead5e0a589a --- /dev/null +++ b/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs @@ -0,0 +1,26 @@ +using System.Net; +using Altinn.Studio.Designer.Exceptions.Options; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Altinn.Studio.Designer.Filters.Options; + +public class OptionsExceptionFilterAttribute : ExceptionFilterAttribute +{ + public override void OnException(ExceptionContext context) + { + base.OnException(context); + + if (context.ActionDescriptor is not ControllerActionDescriptor) + { + return; + } + + if (context.Exception is InvalidOptionsFormatException) + { + context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, OptionsErrorCodes.InvalidOptionsFormat, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest }; + } + } + +} diff --git a/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs b/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs new file mode 100644 index 00000000000..c4a556f977c --- /dev/null +++ b/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers; + +public class NotNullableAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value == null) + { + return new ValidationResult("The field is required."); + } + return ValidationResult.Success; + } +} diff --git a/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs b/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs index a15ad94deeb..b04d6d64ff3 100644 --- a/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs +++ b/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs @@ -1,10 +1,11 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Altinn.Studio.Designer.Exceptions.Options; namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers; -public class OptionConverter : JsonConverter +public class OptionValueConverter : JsonConverter { public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -14,7 +15,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS JsonTokenType.Number when reader.TryGetDouble(out double d) => d, JsonTokenType.True => true, JsonTokenType.False => false, - _ => throw new JsonException($"Unsupported JSON token for Option.Value: {reader.TokenType}") + _ => throw new InvalidOptionsFormatException($"Unsupported JSON token for Option property, {typeToConvert}: {reader.TokenType}.") }; } @@ -32,7 +33,7 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp writer.WriteBooleanValue(b); break; default: - throw new JsonException("Unsupported type for Option.Value."); + throw new InvalidOptionsFormatException($"{value} is an unsupported type for Option fields."); } } } diff --git a/backend/src/Designer/Infrastructure/MvcConfiguration.cs b/backend/src/Designer/Infrastructure/MvcConfiguration.cs index b135b5a35cc..990b05a5498 100644 --- a/backend/src/Designer/Infrastructure/MvcConfiguration.cs +++ b/backend/src/Designer/Infrastructure/MvcConfiguration.cs @@ -2,6 +2,7 @@ using Altinn.Studio.Designer.Filters.DataModeling; using Altinn.Studio.Designer.Filters.Git; using Altinn.Studio.Designer.Filters.IO; +using Altinn.Studio.Designer.Filters.Options; using Altinn.Studio.Designer.ModelBinding; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -26,6 +27,7 @@ public static IServiceCollection ConfigureMvc(this IServiceCollection services) options.Filters.Add(typeof(DataModelingExceptionFilterAttribute)); options.Filters.Add(typeof(GitExceptionFilterAttribute)); options.Filters.Add(typeof(IoExceptionFilterAttribute)); + options.Filters.Add(typeof(OptionsExceptionFilterAttribute)); }) .AddNewtonsoftJson(options => options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter())); diff --git a/backend/src/Designer/Models/Option.cs b/backend/src/Designer/Models/Option.cs index ddfa6b9559e..ad9b3b3b6b8 100644 --- a/backend/src/Designer/Models/Option.cs +++ b/backend/src/Designer/Models/Option.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Altinn.Studio.Designer.Helpers.JsonConverterHelpers; @@ -12,15 +11,15 @@ public class Option /// /// Value that connects the option to the data model. /// - [Required] + [NotNullable] [JsonPropertyName("value")] - [JsonConverter(typeof(OptionConverter))] + [JsonConverter(typeof(OptionValueConverter))] public object Value { get; set; } /// /// Label to present to the user. /// - [Required] + [NotNullable] [JsonPropertyName("label")] public string Label { get; set; } diff --git a/backend/src/Designer/Services/Implementation/OptionsService.cs b/backend/src/Designer/Services/Implementation/OptionsService.cs index f1834c3156a..71ba8b02404 100644 --- a/backend/src/Designer/Services/Implementation/OptionsService.cs +++ b/backend/src/Designer/Services/Implementation/OptionsService.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Altinn.Studio.Designer.Exceptions.Options; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; using LibGit2Sharp; @@ -73,11 +74,7 @@ public async Task> UploadNewOption(string org, string repo, string List