feat(apiv2): better model validation error UX

This commit is contained in:
spiral 2021-10-13 08:37:34 -04:00
parent 5add31c77e
commit 098d804344
No known key found for this signature in database
GPG Key ID: A6059F0CA0E1BD31
15 changed files with 247 additions and 186 deletions

View File

@ -58,21 +58,21 @@ namespace PluralKit.API
await using var tx = await conn.BeginTransactionAsync();
var member = await _repo.CreateMember(systemId, properties.Value<string>("name"), conn);
MemberPatch patch;
try
{
patch = MemberPatch.FromJSON(properties);
var patch = MemberPatch.FromJSON(properties);
patch.AssertIsValid();
}
catch (FieldTooLongError e)
if (patch.Errors.Count > 0)
{
await tx.RollbackAsync();
return BadRequest(e.Message);
}
catch (ValidationError e)
{
await tx.RollbackAsync();
return BadRequest($"Request field '{e.Message}' is invalid.");
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
return BadRequest(err.Text);
else
return BadRequest($"Field {err.Key} is invalid.");
}
member = await _repo.UpdateMember(member.Id, patch, conn);
@ -90,19 +90,19 @@ namespace PluralKit.API
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
MemberPatch patch;
try
{
patch = MemberPatch.FromJSON(changes);
var patch = MemberPatch.FromJSON(changes);
patch.AssertIsValid();
}
catch (FieldTooLongError e)
if (patch.Errors.Count > 0)
{
return BadRequest(e.Message);
}
catch (ValidationError e)
{
return BadRequest($"Request field '{e.Message}' is invalid.");
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
return BadRequest(err.Text);
else
return BadRequest($"Field {err.Key} is invalid.");
}
var newMember = await _repo.UpdateMember(member.Id, patch);

View File

@ -133,19 +133,17 @@ namespace PluralKit.API
{
var system = await _repo.GetSystem(User.CurrentSystem());
SystemPatch patch;
try
{
patch = SystemPatch.FromJSON(changes);
var patch = SystemPatch.FromJSON(changes);
patch.AssertIsValid();
}
catch (FieldTooLongError e)
if (patch.Errors.Count > 0)
{
return BadRequest(e.Message);
}
catch (ValidationError e)
{
return BadRequest($"Request field '{e.Message}' is invalid.");
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
return BadRequest($"Field {err.Key} is invalid.");
}
system = await _repo.UpdateSystem(system!.Id, patch);

View File

@ -55,17 +55,11 @@ namespace PluralKit.API
else
memberId = settings.AutoproxyMember;
SystemGuildPatch patch = null;
try
{
patch = SystemGuildPatch.FromJson(data, memberId);
var patch = SystemGuildPatch.FromJson(data, memberId);
patch.AssertIsValid();
}
catch (ValidationError e)
{
// todo
return BadRequest(e.Message);
}
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
// this is less than great, but at least it's legible
if (patch.AutoproxyMember.Value == null)
@ -116,17 +110,11 @@ namespace PluralKit.API
if (settings == null)
throw APIErrors.MemberGuildNotFound;
MemberGuildPatch patch = null;
try
{
patch = MemberGuildPatch.FromJson(data);
var patch = MemberGuildPatch.FromJson(data);
patch.AssertIsValid();
}
catch (ValidationError e)
{
// todo
return BadRequest(e.Message);
}
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
var newSettings = await _repo.UpdateMemberGuild(member.Id, guild_id, patch);
return Ok(newSettings.ToJson());

View File

@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
{
public class PKError: Exception
@ -25,15 +28,49 @@ namespace PluralKit.API
public class ModelParseError: PKError
{
public ModelParseError() : base(400, 40001, "Error parsing JSON model")
private IEnumerable<ValidationError> _errors { get; init; }
public ModelParseError(IEnumerable<ValidationError> errors) : base(400, 40001, "Error parsing JSON model")
{
// todo
_errors = errors;
}
public new JObject ToJson()
{
var j = base.ToJson();
var e = new JObject();
foreach (var err in _errors)
{
var o = new JObject();
if (err is FieldTooLongError fe)
{
o.Add("message", $"Field {err.Key} is too long.");
o.Add("actual_length", fe.ActualLength);
o.Add("max_length", fe.MaxLength);
}
else if (err.Text != null)
o.Add("message", err.Text);
else
o.Add("message", $"Field {err.Key} is invalid.");
if (e[err.Key] != null)
{
if (e[err.Key].Type == JTokenType.Object)
{
var current = e[err.Key];
e.Remove(err.Key);
e.Add(err.Key, new JArray());
(e[err.Key] as JArray).Add(current);
}
(e[err.Key] as JArray).Add(o);
}
else
e.Add(err.Key, o);
}
j.Add("errors", e);
return j;
}
}

View File

@ -154,6 +154,15 @@ namespace PluralKit.API
return;
}
// for some reason, if we don't specifically cast to ModelParseError, it uses the base's ToJson method
if (exc.Error is ModelParseError fe)
{
ctx.Response.StatusCode = fe.ResponseCode;
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(fe.ToJson()));
return;
}
var err = (PKError)exc.Error;
ctx.Response.StatusCode = err.ResponseCode;

View File

@ -0,0 +1,29 @@
using System;
using Newtonsoft.Json;
namespace PluralKit.Core
{
public class ValidationError
{
public string Key;
public string? Text;
public ValidationError(string key, string? text = null)
{
Key = key;
Text = text;
}
}
public class FieldTooLongError: ValidationError
{
public int MaxLength;
public int ActualLength;
public FieldTooLongError(string key, int maxLength, int actualLength) : base(key)
{
MaxLength = maxLength;
ActualLength = actualLength;
}
}
}

View File

@ -38,7 +38,7 @@ namespace PluralKit.Core
public new void AssertIsValid()
{
if (Name.IsPresent)
if (Name.Value != null)
AssertValid(Name.Value, "name", Limits.MaxGroupNameLength);
if (DisplayName.Value != null)
AssertValid(DisplayName.Value, "display_name", Limits.MaxGroupNameLength);
@ -59,10 +59,13 @@ namespace PluralKit.Core
{
var patch = new GroupPatch();
if (o.ContainsKey("name") && o["name"].Type == JTokenType.Null)
throw new ValidationError("Group name can not be set to null.");
if (o.ContainsKey("name"))
{
patch.Name = o.Value<string>("name").NullIfEmpty();
if (patch.Name.Value == null)
patch.Errors.Add(new ValidationError("name", "Group name can not be set to null."));
}
if (o.ContainsKey("name")) patch.Name = o.Value<string>("name");
if (o.ContainsKey("display_name")) patch.DisplayName = o.Value<string>("display_name").NullIfEmpty();
if (o.ContainsKey("description")) patch.Description = o.Value<string>("description").NullIfEmpty();
if (o.ContainsKey("icon")) patch.Icon = o.Value<string>("icon").NullIfEmpty();
@ -74,16 +77,16 @@ namespace PluralKit.Core
var privacy = o.Value<JObject>("privacy");
if (privacy.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = privacy.ParsePrivacy("description_privacy");
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy");
if (privacy.ContainsKey("icon_privacy"))
patch.IconPrivacy = privacy.ParsePrivacy("icon_privacy");
patch.IconPrivacy = patch.ParsePrivacy(privacy, "icon_privacy");
if (privacy.ContainsKey("list_privacy"))
patch.ListPrivacy = privacy.ParsePrivacy("list_privacy");
patch.ListPrivacy = patch.ParsePrivacy(privacy, "list_privacy");
if (privacy.ContainsKey("visibility"))
patch.Visibility = privacy.ParsePrivacy("visibility");
patch.Visibility = patch.ParsePrivacy(privacy, "visibility");
}
return patch;

View File

@ -58,7 +58,7 @@ namespace PluralKit.Core
public new void AssertIsValid()
{
if (Name.IsPresent)
if (Name.Value != null)
AssertValid(Name.Value, "name", Limits.MaxMemberNameLength);
if (DisplayName.Value != null)
AssertValid(DisplayName.Value, "display_name", Limits.MaxMemberNameLength);
@ -77,7 +77,7 @@ namespace PluralKit.Core
if (ProxyTags.IsPresent && (ProxyTags.Value.Length > 100 ||
ProxyTags.Value.Any(tag => tag.ProxyString.IsLongerThan(100))))
// todo: have a better error for this
throw new ValidationError("proxy_tags");
Errors.Add(new ValidationError("proxy_tags"));
}
#nullable disable
@ -86,8 +86,12 @@ namespace PluralKit.Core
{
var patch = new MemberPatch();
if (o.ContainsKey("name") && o["name"].Type == JTokenType.Null)
throw new ValidationError("Member name can not be set to null.");
if (o.ContainsKey("name"))
{
patch.Name = o.Value<string>("name").NullIfEmpty();
if (patch.Name.Value == null)
patch.Errors.Add(new ValidationError("name", "Member name can not be set to null."));
}
if (o.ContainsKey("name")) patch.Name = o.Value<string>("name");
if (o.ContainsKey("color")) patch.Color = o.Value<string>("color").NullIfEmpty()?.ToLower();
@ -101,7 +105,7 @@ namespace PluralKit.Core
var res = DateTimeFormats.DateExportFormat.Parse(str);
if (res.Success) patch.Birthday = res.Value;
else if (str == null) patch.Birthday = null;
else throw new ValidationError("birthday");
else patch.Errors.Add(new ValidationError("birthday"));
}
if (o.ContainsKey("pronouns")) patch.Pronouns = o.Value<string>("pronouns").NullIfEmpty();
@ -123,7 +127,7 @@ namespace PluralKit.Core
if (o.ContainsKey("privacy"))
{
var plevel = o.ParsePrivacy("privacy");
var plevel = patch.ParsePrivacy(o, "privacy");
patch.Visibility = plevel;
patch.NamePrivacy = plevel;
@ -136,14 +140,14 @@ namespace PluralKit.Core
}
else
{
if (o.ContainsKey("visibility")) patch.Visibility = o.ParsePrivacy("visibility");
if (o.ContainsKey("name_privacy")) patch.NamePrivacy = o.ParsePrivacy("name_privacy");
if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = o.ParsePrivacy("description_privacy");
if (o.ContainsKey("avatar_privacy")) patch.AvatarPrivacy = o.ParsePrivacy("avatar_privacy");
if (o.ContainsKey("birthday_privacy")) patch.BirthdayPrivacy = o.ParsePrivacy("birthday_privacy");
if (o.ContainsKey("pronoun_privacy")) patch.PronounPrivacy = o.ParsePrivacy("pronoun_privacy");
if (o.ContainsKey("visibility")) patch.Visibility = patch.ParsePrivacy(o, "visibility");
if (o.ContainsKey("name_privacy")) patch.NamePrivacy = patch.ParsePrivacy(o, "name_privacy");
if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy");
if (o.ContainsKey("avatar_privacy")) patch.AvatarPrivacy = patch.ParsePrivacy(o, "avatar_privacy");
if (o.ContainsKey("birthday_privacy")) patch.BirthdayPrivacy = patch.ParsePrivacy(o, "birthday_privacy");
if (o.ContainsKey("pronoun_privacy")) patch.PronounPrivacy = patch.ParsePrivacy(o, "pronoun_privacy");
// if (o.ContainsKey("color_privacy")) member.ColorPrivacy = o.ParsePrivacy("member");
if (o.ContainsKey("metadata_privacy")) patch.MetadataPrivacy = o.ParsePrivacy("metadata_privacy");
if (o.ContainsKey("metadata_privacy")) patch.MetadataPrivacy = patch.ParsePrivacy(o, "metadata_privacy");
}
break;
}
@ -161,25 +165,25 @@ namespace PluralKit.Core
var privacy = o.Value<JObject>("privacy");
if (privacy.ContainsKey("visibility"))
patch.Visibility = privacy.ParsePrivacy("visibility");
patch.Visibility = patch.ParsePrivacy(privacy, "visibility");
if (privacy.ContainsKey("name_privacy"))
patch.NamePrivacy = privacy.ParsePrivacy("name_privacy");
patch.NamePrivacy = patch.ParsePrivacy(privacy, "name_privacy");
if (privacy.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = privacy.ParsePrivacy("description_privacy");
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy");
if (privacy.ContainsKey("avatar_privacy"))
patch.AvatarPrivacy = privacy.ParsePrivacy("avatar_privacy");
patch.AvatarPrivacy = patch.ParsePrivacy(privacy, "avatar_privacy");
if (privacy.ContainsKey("birthday_privacy"))
patch.BirthdayPrivacy = privacy.ParsePrivacy("birthday_privacy");
patch.BirthdayPrivacy = patch.ParsePrivacy(privacy, "birthday_privacy");
if (privacy.ContainsKey("pronoun_privacy"))
patch.PronounPrivacy = privacy.ParsePrivacy("pronoun_privacy");
patch.PronounPrivacy = patch.ParsePrivacy(privacy, "pronoun_privacy");
if (privacy.ContainsKey("metadata_privacy"))
patch.MetadataPrivacy = privacy.ParsePrivacy("metadata_privacy");
patch.MetadataPrivacy = patch.ParsePrivacy(privacy, "metadata_privacy");
}
break;

View File

@ -1,50 +1,46 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using SqlKata;
namespace PluralKit.Core
{
public abstract class PatchObject
{
public List<ValidationError> Errors = new();
public abstract Query Apply(Query q);
public void AssertIsValid() { }
protected bool AssertValid(string input, string name, int maxLength, Func<string, bool>? validate = null)
protected void AssertValid(string input, string name, int maxLength, Func<string, bool>? validate = null)
{
if (input.Length > maxLength)
throw new FieldTooLongError(name, maxLength, input.Length);
Errors.Add(new FieldTooLongError(name, maxLength, input.Length));
if (validate != null && !validate(input))
throw new ValidationError(name);
return true;
Errors.Add(new ValidationError(name));
}
protected bool AssertValid(string input, string name, string pattern)
protected void AssertValid(string input, string name, string pattern)
{
if (!Regex.IsMatch(input, pattern))
throw new ValidationError(name);
return true;
}
Errors.Add(new ValidationError(name));
}
public class ValidationError: Exception
public PrivacyLevel ParsePrivacy(JObject o, string propertyName)
{
public ValidationError(string message) : base(message) { }
}
var input = o.Value<string>(propertyName);
public class FieldTooLongError: ValidationError
{
public string Name;
public int MaxLength;
public int ActualLength;
if (input == null) return PrivacyLevel.Public;
if (input == "") return PrivacyLevel.Private;
if (input == "private") return PrivacyLevel.Private;
if (input == "public") return PrivacyLevel.Public;
public FieldTooLongError(string name, int maxLength, int actualLength) :
base($"{name} too long ({actualLength} > {maxLength})")
{
Name = name;
MaxLength = maxLength;
ActualLength = actualLength;
Errors.Add(new ValidationError(propertyName));
// unused, but the compiler will complain if this isn't here
return PrivacyLevel.Private;
}
}
}

View File

@ -36,8 +36,14 @@ namespace PluralKit.Core
if (o.ContainsKey("proxying_enabled") && o["proxying_enabled"].Type != JTokenType.Null)
patch.ProxyEnabled = o.Value<bool>("proxying_enabled");
if (o.ContainsKey("autoproxy_mode") && o["autoproxy_mode"].ParseAutoproxyMode() is { } autoproxyMode)
patch.AutoproxyMode = autoproxyMode;
if (o.ContainsKey("autoproxy_mode"))
{
var (val, err) = o["autoproxy_mode"].ParseAutoproxyMode();
if (err != null)
patch.Errors.Add(err);
else
patch.AutoproxyMode = val.Value;
}
patch.AutoproxyMember = memberId;

View File

@ -69,7 +69,7 @@ namespace PluralKit.Core
if (Color.Value != null)
AssertValid(Color.Value, "color", "^[0-9a-fA-F]{6}$");
if (UiTz.IsPresent && DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz.Value) == null)
throw new ValidationError("avatar_url");
Errors.Add(new ValidationError("timezone"));
}
#nullable disable
@ -91,10 +91,10 @@ namespace PluralKit.Core
{
if (o.ContainsKey("tz")) patch.UiTz = o.Value<string>("tz") ?? "UTC";
if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = o.ParsePrivacy("description_privacy");
if (o.ContainsKey("member_list_privacy")) patch.MemberListPrivacy = o.ParsePrivacy("member_list_privacy");
if (o.ContainsKey("front_privacy")) patch.FrontPrivacy = o.ParsePrivacy("front_privacy");
if (o.ContainsKey("front_history_privacy")) patch.FrontHistoryPrivacy = o.ParsePrivacy("front_history_privacy");
if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy");
if (o.ContainsKey("member_list_privacy")) patch.MemberListPrivacy = patch.ParsePrivacy(o, "member_list_privacy");
if (o.ContainsKey("front_privacy")) patch.FrontPrivacy = patch.ParsePrivacy(o, "front_privacy");
if (o.ContainsKey("front_history_privacy")) patch.FrontHistoryPrivacy = patch.ParsePrivacy(o, "front_history_privacy");
break;
}
@ -105,16 +105,16 @@ namespace PluralKit.Core
var privacy = o.Value<JObject>("privacy");
if (privacy.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = privacy.ParsePrivacy("description_privacy");
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy");
if (privacy.ContainsKey("member_list_privacy"))
patch.DescriptionPrivacy = privacy.ParsePrivacy("member_list_privacy");
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "member_list_privacy");
if (privacy.ContainsKey("front_privacy"))
patch.DescriptionPrivacy = privacy.ParsePrivacy("front_privacy");
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "front_privacy");
if (privacy.ContainsKey("front_history_privacy"))
patch.DescriptionPrivacy = privacy.ParsePrivacy("front_history_privacy");
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "front_history_privacy");
}
break;

View File

@ -1,7 +1,5 @@
using System;
using Newtonsoft.Json.Linq;
namespace PluralKit.Core
{
public enum PrivacyLevel
@ -42,18 +40,5 @@ namespace PluralKit.Core
}
public static string ToJsonString(this PrivacyLevel level) => level.LevelName();
public static PrivacyLevel ParsePrivacy(this JObject o, string propertyName)
{
var input = o.Value<string>(propertyName);
if (input == null) return PrivacyLevel.Public;
if (input == "") return PrivacyLevel.Private;
if (input == "private") return PrivacyLevel.Private;
if (input == "public") return PrivacyLevel.Public;
throw new ValidationError(propertyName);
}
}
}

View File

@ -41,27 +41,27 @@ namespace PluralKit.Core
return o;
}
public static AutoproxyMode? ParseAutoproxyMode(this JToken o)
public static (AutoproxyMode?, ValidationError?) ParseAutoproxyMode(this JToken o)
{
if (o.Type == JTokenType.Null)
return AutoproxyMode.Off;
return (AutoproxyMode.Off, null);
else if (o.Type != JTokenType.String)
return null;
return (null, new ValidationError("autoproxy_mode"));
var value = o.Value<string>();
switch (value)
{
case "off":
return AutoproxyMode.Off;
return (AutoproxyMode.Off, null);
case "front":
return AutoproxyMode.Front;
return (AutoproxyMode.Front, null);
case "latch":
return AutoproxyMode.Latch;
return (AutoproxyMode.Latch, null);
case "member":
return AutoproxyMode.Member;
return (AutoproxyMode.Member, null);
default:
throw new ValidationError($"Value '{value}' is not a valid autoproxy mode.");
return (null, new ValidationError("autoproxy_mode", $"Value '{value}' is not a valid autoproxy mode."));
}
}
}

View File

@ -20,13 +20,17 @@ namespace PluralKit.Core
{
var patch = SystemPatch.FromJSON(importFile);
try
{
patch.AssertIsValid();
}
catch (ValidationError e)
if (patch.Errors.Count > 0)
{
throw new ImportException($"Field {e.Message} in export file is invalid.");
var err = patch.Errors[0];
if (err is FieldTooLongError)
throw new ImportException($"Field {err.Key} in export file is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
throw new ImportException(err.Text);
else
throw new ImportException($"Field {err.Key} in export file is invalid.");
}
await _repo.UpdateSystem(_system.Id, patch, _conn);
@ -87,17 +91,18 @@ namespace PluralKit.Core
);
var patch = MemberPatch.FromJSON(member);
try
{
patch.AssertIsValid();
}
catch (FieldTooLongError e)
if (patch.Errors.Count > 0)
{
throw new ImportException($"Field {e.Name} in member {referenceName} is too long ({e.ActualLength} > {e.MaxLength}).");
}
catch (ValidationError e)
{
throw new ImportException($"Field {e.Message} in member {referenceName} is invalid.");
var err = patch.Errors[0];
if (err is FieldTooLongError)
throw new ImportException($"Field {err.Key} in member {name} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
throw new ImportException($"member {name}: {err.Text}");
else
throw new ImportException($"Field {err.Key} in member {name} is invalid.");
}
MemberId? memberId = found;
@ -128,17 +133,18 @@ namespace PluralKit.Core
);
var patch = GroupPatch.FromJson(group);
try
{
patch.AssertIsValid();
}
catch (FieldTooLongError e)
if (patch.Errors.Count > 0)
{
throw new ImportException($"Field {e.Name} in group {referenceName} is too long ({e.ActualLength} > {e.MaxLength}).");
}
catch (ValidationError e)
{
throw new ImportException($"Field {e.Message} in group {referenceName} is invalid.");
var err = patch.Errors[0];
if (err is FieldTooLongError)
throw new ImportException($"Field {err.Key} in group {name} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
throw new ImportException($"group {name}: {err.Text}");
else
throw new ImportException($"Field {err.Key} in group {name} is invalid.");
}
GroupId? groupId = found;

View File

@ -87,6 +87,19 @@ namespace PluralKit.Core
patch.DisplayName = $"{name} {tag}";
}
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
var err = patch.Errors[0];
if (err is FieldTooLongError)
throw new ImportException($"Field {err.Key} in tupper {name} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
throw new ImportException($"tupper {name}: {err.Text}");
else
throw new ImportException($"Field {err.Key} in tupper {name} is invalid.");
}
var isNewMember = false;
if (!_existingMemberNames.TryGetValue(name, out var memberId))
{
@ -101,19 +114,6 @@ namespace PluralKit.Core
_logger.Debug("Importing member with identifier {FileId} to system {System} (is creating new member? {IsCreatingNewMember})",
name, _system.Id, isNewMember);
try
{
patch.AssertIsValid();
}
catch (FieldTooLongError e)
{
throw new ImportException($"Field {e.Name} in tupper {name} is too long ({e.ActualLength} > {e.MaxLength}).");
}
catch (ValidationError e)
{
throw new ImportException($"Field {e.Message} in tupper {name} is invalid.");
}
await _repo.UpdateMember(memberId, patch, _conn);
return (lastSetTag, multipleTags, hasGroup);