Implement the dynamic crud endpoint

This commit is contained in:
Daniel_I_Am 2020-10-13 01:15:40 +02:00
parent e78c0d8746
commit 8d072d48eb
No known key found for this signature in database
GPG Key ID: 80C428FCC9743E84
5 changed files with 221 additions and 7 deletions

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using ChaosBot.Models;
using ChaosBot.WebServer.Services;
@ -15,12 +16,15 @@ namespace ChaosBot.WebServer.App.ApiControllers
public abstract class BaseApiController<T, TDeleteParameter> : Controller where T : class, new()
{
protected readonly AccessTokenCache AccessTokenCache;
protected readonly ValidationService ValidationService;
protected BaseApiController(
AccessTokenCache accessTokenCache
AccessTokenCache accessTokenCache,
ValidationService validationService
)
{
AccessTokenCache = accessTokenCache;
ValidationService = validationService;
}
public async Task<IActionResult> Index(ulong guildId)
@ -50,10 +54,13 @@ namespace ChaosBot.WebServer.App.ApiControllers
return Content(JsonConvert.SerializeObject(response), new MediaTypeHeaderValue("application/json"));
}
public async Task<IActionResult> Upsert(ulong guildId, dynamic requestBody)
public async Task<IActionResult> Upsert(ulong guildId, JsonElement requestBody)
{
if (!CheckPermissions.GetResult(AccessTokenCache, Request, guildId, out IActionResult result))
return result;
if (!ValidationService.Validate(requestBody, GetValidationRules(), out string errors))
return BadRequest(errors);
await using ChaosbotContext dbContext = new ChaosbotContext();

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using ChaosBot.Models;
using ChaosBot.WebServer.Models;
@ -15,9 +16,11 @@ namespace ChaosBot.WebServer.App.ApiControllers
public class CustomCommandController : BaseApiController<CustomCommand, string>
{
public CustomCommandController(
AccessTokenCache accessTokenCache
AccessTokenCache accessTokenCache,
ValidationService validationService
) : base(
accessTokenCache
accessTokenCache,
validationService
) {}
[HttpGet]
@ -31,7 +34,7 @@ namespace ChaosBot.WebServer.App.ApiControllers
[Route("{guildId}")]
public async Task<IActionResult> UpsertAction(
[FromRoute] ulong guildId,
[FromBody] dynamic requestBody)
[FromBody] JsonElement requestBody)
{
return await Upsert(guildId, requestBody);
}
@ -64,7 +67,12 @@ namespace ChaosBot.WebServer.App.ApiControllers
protected override Dictionary<string, List<string>> GetValidationRules()
{
return new Dictionary<string, List<string>>();
return new Dictionary<string, List<string>>
{
{"Command", new List<string>{"required", "type:string", "min:1", "max:128"}},
{"Type", new List<string>{"required", "type:integer", "in:CustomCommandType"}},
{"Content", new List<string>{"required", "type:string"}},
};
}
protected override UpsertCommandBuilder<CustomCommand> ApplyFilterForUpsert(

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace ChaosBot.WebServer.Services
{
public class ValidationService
{
public bool Validate(JsonElement requestBody, Dictionary<string,List<string>> getValidationRules, out string error)
{
StringBuilder errorBuilder = new StringBuilder();
foreach (KeyValuePair<string,List<string>> validationRule in getValidationRules)
{
string key = validationRule.Key;
dynamic value = GetValueFromRequest(requestBody, key);
List<string> rules = validationRule.Value;
foreach (string rule in rules)
{
string[] ruleParts = rule.Split(':');
string ruleType = ruleParts.First();
if (ruleType == null)
continue;
ValidationResult result = ruleType switch
{
"required" => CheckRequired(key, value),
"type" => CheckType(key, value, ruleParts),
"min" => CheckMin(key, value, ruleParts),
"max" => CheckMax(key, value, ruleParts),
_ => new ValidationResult.Unknown()
};
if (result.GetError() != null)
{
errorBuilder.AppendLine($"[{result.GetKey()}] {result.GetError()}");
break;
}
}
}
error = errorBuilder.ToString();
return error.Length == 0;
}
private static dynamic GetValueFromRequest(JsonElement requestBody, string key)
{
JsonElement prop;
try
{
prop = requestBody.GetProperty(key);
}
catch (KeyNotFoundException)
{
return null;
}
return prop.ValueKind switch
{
JsonValueKind.String => prop.GetString(),
JsonValueKind.Number => prop.GetInt64(),
JsonValueKind.True => prop.GetBoolean(),
JsonValueKind.False => prop.GetBoolean(),
_ => null
};
}
private ValidationResult CheckRequired(string key, dynamic value)
{
if (value != null)
return new ValidationResult.Success();
return new ValidationResult.Failure(key, $"{key} is required");
}
private ValidationResult CheckType(string key, dynamic value, string[] ruleParts)
{
if (ruleParts.Length != 2) return new ValidationResult.Unknown();
Type type = ruleParts[1] switch
{
"string" => typeof(string),
"boolean" => typeof(bool),
"short" => typeof(short),
"ushort" => typeof(ushort),
"int" => typeof(int),
"uint" => typeof(uint),
"long" => typeof(long),
"ulong" => typeof(ulong),
_ => null
};
if (type == null)
return new ValidationResult.Unknown();
dynamic val = Convert.ChangeType(value, type);
if (val != null)
return new ValidationResult.Success();
return new ValidationResult.Failure(key, $"{key} could not be interpreted as {ruleParts[1]}");
}
private ValidationResult CheckMin(string key, dynamic value, string[] ruleParts)
{
if (ruleParts.Length != 2) return new ValidationResult.Unknown();
if (value is string stringValue)
{
int minLength = Convert.ToInt32(ruleParts[1]);
if (stringValue.Length < minLength)
return new ValidationResult.Failure(key, $"{key} cannot be shorter than {minLength} characters");
} else if (value is ulong intValue)
{
ulong minSize = Convert.ToUInt64(ruleParts[1]);
if (intValue < minSize)
return new ValidationResult.Failure(key, $"{key} must be greater than or equal to {minSize}");
} else if (value is double floatValue)
{
double minSize = Convert.ToDouble(ruleParts[1]);
if (floatValue < minSize)
return new ValidationResult.Failure(key, $"{key} must be greater than or equal to {minSize}");
}
else
{
return new ValidationResult.Unknown();
}
return new ValidationResult.Success();
}
private ValidationResult CheckMax(string key, dynamic value, string[] ruleParts)
{
if (ruleParts.Length != 2) return new ValidationResult.Unknown();
if (value is string stringValue)
{
int maxLength = Convert.ToInt32(ruleParts[1]);
if (stringValue.Length > maxLength)
return new ValidationResult.Failure(key, $"{key} cannot be longer than {maxLength} characters");
} else if (value is ulong intValue)
{
ulong maxSize = Convert.ToUInt64(ruleParts[1]);
if (intValue > maxSize)
return new ValidationResult.Failure(key, $"{key} must be less than or equal to {maxSize}");
} else if (value is double floatValue)
{
double maxSize = Convert.ToDouble(ruleParts[1]);
if (floatValue > maxSize)
return new ValidationResult.Failure(key, $"{key} must be less than or equal to {maxSize}");
}
else
{
return new ValidationResult.Unknown();
}
return new ValidationResult.Success();
}
}
internal class ValidationResult
{
private readonly string _errorMessage;
private readonly string _key;
private ValidationResult()
{
_errorMessage = null;
_key = null;
}
private ValidationResult(string key, string errorMessage)
{
_errorMessage = errorMessage;
_key = key;
}
internal class Success : ValidationResult {}
internal class Failure : ValidationResult
{
public Failure(string key, string message) : base(key, message) {}
}
internal class Unknown : ValidationResult {}
public string GetError()
{
return this._errorMessage;
}
public string GetKey()
{
return this._key;
}
}
}

View File

@ -33,6 +33,7 @@ namespace ChaosBot.WebServer
.AddSingleton(sp => new AccessTokenCache())
.AddSingleton(sp => new DiscordInviteGenerator())
.AddSingleton(sp => new HttpClient())
.AddSingleton(sp => new ValidationService())
;
}

@ -1 +1 @@
Subproject commit 07d2c3dc0e7ba5bfaa8eac94dad814f1a333a6eb
Subproject commit 56e8caf708d791a2b05b12e82fd407905147fafd